Compare commits
6 Commits
1.42.0.69-
...
1.12.1.1-D
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e898ea636 | ||
|
|
2faec601c4 | ||
|
|
57b7b8cee9 | ||
|
|
b492d12e19 | ||
|
|
6bd59d01aa | ||
|
|
371b1c2fb1 |
@@ -6,9 +6,7 @@ on:
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: LightlessSync
|
||||
DOTNET_VERSION: |
|
||||
10.x.x
|
||||
9.x.x
|
||||
DOTNET_VERSION: 9.x
|
||||
|
||||
jobs:
|
||||
tag-and-release:
|
||||
@@ -18,17 +16,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Lightless
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET 10 SDK
|
||||
uses: actions/setup-dotnet@v5
|
||||
- name: Setup .NET 9 SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: |
|
||||
10.x.x
|
||||
9.x.x
|
||||
dotnet-version: 9.x
|
||||
|
||||
- name: Download Dalamud
|
||||
run: |
|
||||
|
||||
18
.gitmodules
vendored
18
.gitmodules
vendored
@@ -1,18 +1,6 @@
|
||||
[submodule "LightlessAPI"]
|
||||
path = LightlessAPI
|
||||
url = https://git.lightless-sync.org/Lightless-Sync/LightlessAPI.git
|
||||
[submodule "Penumbra.GameData"]
|
||||
path = Penumbra.GameData
|
||||
url = https://github.com/Ottermandias/Penumbra.GameData
|
||||
[submodule "Penumbra.Api"]
|
||||
path = Penumbra.Api
|
||||
url = https://github.com/Ottermandias/Penumbra.Api
|
||||
[submodule "Penumbra.String"]
|
||||
path = Penumbra.String
|
||||
url = https://github.com/Ottermandias/Penumbra.String
|
||||
[submodule "OtterGui"]
|
||||
path = OtterGui
|
||||
url = https://github.com/Ottermandias/OtterGui
|
||||
[submodule "ffxiv_pictomancy"]
|
||||
path = ffxiv_pictomancy
|
||||
url = https://github.com/sourpuh/ffxiv_pictomancy
|
||||
[submodule "PenumbraAPI"]
|
||||
path = PenumbraAPI
|
||||
url = https://github.com/Ottermandias/Penumbra.Api.git
|
||||
|
||||
Submodule LightlessAPI updated: 8e4432af45...6c542c0ccc
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.0.11217.181
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.1.32328.378
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{585B740D-BA2C-429B-9CF3-B2D223423748}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
@@ -12,110 +12,40 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessSync", "LightlessS
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessSync.API", "LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj", "{A4E42AFA-5045-7E81-937F-3A320AC52987}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.Api", "Penumbra.Api\Penumbra.Api.csproj", "{22AE06C8-5139-45D2-A5F9-E76C019050D9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{3C016B19-2A2C-4068-9378-B9B805605EFB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OtterGui", "OtterGui\OtterGui.csproj", "{C77A2833-3FE4-405B-811D-439B1FF859D9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pictomancy", "ffxiv_pictomancy\Pictomancy\Pictomancy.csproj", "{825F17D8-2704-24F6-DF8B-2542AC92C765}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.Api", "PenumbraAPI\Penumbra.Api.csproj", "{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.ActiveCfg = Release|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.Build.0 = Release|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x64.Build.0 = Debug|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|Any CPU.Build.0 = Release|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x64.ActiveCfg = Release|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x64.Build.0 = Release|x64
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x86.Build.0 = Release|Any CPU
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|x64.Build.0 = Debug|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|x86.Build.0 = Debug|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|Any CPU.Build.0 = Release|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|x64.ActiveCfg = Release|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|x64.Build.0 = Release|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|x86.ActiveCfg = Release|x64
|
||||
{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|x86.Build.0 = Release|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|x64.Build.0 = Debug|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|x86.Build.0 = Debug|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|Any CPU.Build.0 = Release|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|x64.ActiveCfg = Release|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|x64.Build.0 = Release|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|x86.ActiveCfg = Release|x64
|
||||
{22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|x86.Build.0 = Release|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|x64.Build.0 = Debug|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|x86.Build.0 = Debug|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|Any CPU.Build.0 = Release|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|x64.ActiveCfg = Release|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|x64.Build.0 = Release|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|x86.ActiveCfg = Release|x64
|
||||
{3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|x86.Build.0 = Release|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|x64.Build.0 = Debug|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|x86.Build.0 = Debug|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|Any CPU.Build.0 = Release|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x64.ActiveCfg = Release|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x64.Build.0 = Release|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x86.ActiveCfg = Release|x64
|
||||
{C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x86.Build.0 = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x64.Build.0 = Debug|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x86.ActiveCfg = Debug|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x86.Build.0 = Debug|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|Any CPU.Build.0 = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.ActiveCfg = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.Build.0 = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.ActiveCfg = Release|x64
|
||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.Build.0 = Release|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|x64.Build.0 = Debug|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|Any CPU.ActiveCfg = Release|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|Any CPU.Build.0 = Release|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|x64.ActiveCfg = Release|x64
|
||||
{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
tagline: "Lightless Sync v2.0.0"
|
||||
subline: "LIGHTLESS IS EVOLVING!!"
|
||||
changelog:
|
||||
- name: "v2.0.0"
|
||||
tagline: "Thank you for 4 months!"
|
||||
date: "December 2025"
|
||||
# be sure to set this every new version
|
||||
isCurrent: true
|
||||
versions:
|
||||
- number: "Lightless Chat"
|
||||
icon: ""
|
||||
items:
|
||||
- "Chat has been added to the top of the main UI. It will work in certain Zones or in Syncshells!"
|
||||
- "You will only be able to use the chat feature after enabling it and accepting the rules. If you're not interested, don't use it!"
|
||||
- "Breaking the rules may result in a mute or ban from chat. Serious offenses may result in a ban from the Lightless service altogether."
|
||||
- "You can right click the offender in the chat and report them within the chat, reports will be reviewed asap."
|
||||
- "Syncshells can enforce their own chat rules and moderate their own chat. This however does not apply to serious offenses."
|
||||
- "Your name in chat will not be shown unless you are paired with the person OR you are in the same syncshell. Otherwise, you will be anonymous."
|
||||
- "Refer to #release-notes in the Discord for more information. Feel free to ask questions in the Discord as well."
|
||||
- number: "Changes to LightFinder"
|
||||
icon: ""
|
||||
items:
|
||||
- "We have recieve quite a bit of reports of users crashing due to how Nameplates are handled across various plugins. As a result, we have moved the LightFinder icon and text to Imgui."
|
||||
- "This should resolve the crashing issues, however, it may not look as nice as before. We are looking into ways to improve the Imgui experience in the future."
|
||||
- "We will always prioritize stability and safety over visuals."
|
||||
- "Refer to #release-notes in the Discord for an example of the error."
|
||||
- number: "User Profiles, ShellFinder, Syncshells, Syncshell Profiles"
|
||||
icon: ""
|
||||
items:
|
||||
- "Both User Profiles and Syncshell Profiles have been revamped for 2.0.0."
|
||||
- "We have added profile tags to both Users and Syncshells that will show when a profile is being viewed"
|
||||
- "Syncshell Admin Panel has been reworked to make it a friendlier experience"
|
||||
- "Syncshell Moderators can now also broadcast on ShellFinder"
|
||||
- "ShellFinder has been revamped to be more visually friends and also show more information (Tags) about the Syncshell"
|
||||
- "Syncshells has an auto-prune feature now that will remove inactive members after a set amount of time, options available are 1, 3, 7, and 14 days that runs in 1 hour intervals"
|
||||
- "IF YOUR SYNCSHELL IS NSFW, PLEASE MARK IT AS NSFW!"
|
||||
- "Refer to #release-notes in the Discord for pretty pictures or try it yourself!."
|
||||
- number: "Texture Optimization"
|
||||
icon: ""
|
||||
items:
|
||||
- "In 2.0.0, we've added the option for Texture Optimization to improve the performance of scenarios such as overwhelmingly big "
|
||||
- "NOTE: ALL OF THESE ARE OPTIONAL AND DISABLED BY DEFAULT"
|
||||
- "Within Texture Optimization, you will be able to safely downscale all textures of new downloads around you."
|
||||
- "This downscale DOES NOT APPLY to DIRECT PAIRS or those who've updated their preferred settings to not be downscaled"
|
||||
- "The first time this is enabled, you may experience some lag or frame drops, but in the long run, it will help performance."
|
||||
- "This can be found in Lightless Settings > Performance > Texture Optimization"
|
||||
- "Like a broken record, please refer to #release-notes in the Discord for more information."
|
||||
- number: "Character Analysis - The big scary UI no one knew about"
|
||||
icon: ""
|
||||
items:
|
||||
- "We have made the Character Analysis UI more user friendly. This includes a revamp of the look and functionality"
|
||||
- "You can now see more information about your character and how it affects performance"
|
||||
- "It will show you the Textures tab by default with an option for \"Other file types\""
|
||||
- "You can now choose if you want to BC7/BC5/BC4/BC3/BC1 compress a certain texture."
|
||||
- "The UI will give you a recommendation on what BC compression to use based on the file."
|
||||
- "Shows a small preview of what the texture looks like with some general info about it."
|
||||
- "Shows you how much VRAM you would take up."
|
||||
- "This can be found in Lightless Settings > Performance > Character Analysis"
|
||||
- number: "Performance"
|
||||
icon: ""
|
||||
items:
|
||||
- "Moved to the internal object table to have improved overall plugin performance."
|
||||
- "Compactor is now running on a multi-threaded level instead of single-threaded; This should increase the speed of compacting files."
|
||||
- "Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many Syncshells in your list."
|
||||
- "Pairing system has been revamped to make pausing and unpausing faster, and loading people should be faster as well."
|
||||
- number: "Miscellaneous Changes and Bugfixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "UI has been updated to look more modern"
|
||||
- "We have started on file compression for Linux with the option for BTRFS or ZFS but it's not very great yet and will release later."
|
||||
- "Nameplate colours now use sigs to client structs as an alternative to the Nameplate Handler, also preventing crashes on that from our end."
|
||||
- "Notifications now work with the \"Enable multi-monitor windows\" settings of Dalamud."
|
||||
- "Fixed a bug where nothing above the notifications was clickable in certain cases."
|
||||
- "Added a check that prevents small messages from going below 0 resulting in an ArgumentOutOfRangeException."
|
||||
- name: "v1.12.4"
|
||||
tagline: "Preparation for future features"
|
||||
date: "November 11th 2025"
|
||||
versions:
|
||||
- number: "Syncshells"
|
||||
icon: ""
|
||||
items:
|
||||
- "Added a pause button for syncshells in grouped folders"
|
||||
- number: "Notifications"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed download notifications getting stuck at times"
|
||||
- "Added more offset positions for the notifications"
|
||||
- number: "Lightfinder"
|
||||
icon: ""
|
||||
items:
|
||||
- "Pair button will now show up when you're not directly paired. ie. You are technically paired in a syncshell, but you may not be directly paired'"
|
||||
- "Fixed a problem where the number of LightFinder users were cached, displaying the wrong information"
|
||||
- "When LightFinder is enabled, if you hover over the number of users, it will show you how many users are also using LightFinder in your area"
|
||||
-
|
||||
- number: "Bugfixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Added even more checks to nameplate handler to help not only us debug, but also other plugins writing to the plate"
|
||||
- number: "Miscellaneous Changes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Default Linux to Websockets"
|
||||
- "Revised Brio warning"
|
||||
- "Added /lightless command to open Lightless UI (you can still use /light)"
|
||||
- "Initial groundwork for future features"
|
||||
- name: "v1.12.3"
|
||||
tagline: "LightSpeed, Welcome Screen, and More!"
|
||||
date: "October 15th 2025"
|
||||
versions:
|
||||
- number: "LightSpeed"
|
||||
icon: ""
|
||||
items:
|
||||
- "New way to download that will download mods directly from the file server"
|
||||
- "LightSpeed is in BETA and should be faster than the batch downloading"
|
||||
- number: "Welcome Screen + Additional Features"
|
||||
icon: ""
|
||||
items:
|
||||
- "New in-game Patch Notes window."
|
||||
- "Credits section to thank contributors and supporters."
|
||||
- "Patch notes only show after updates, not during first-time setup."
|
||||
- "Syncshell Rework stared: Profiles have been added (more features using this will come later)."
|
||||
- number: "Notifications"
|
||||
icon: ""
|
||||
items:
|
||||
- "More customizable notification options."
|
||||
- "Perfomance limiter shows as notifications."
|
||||
- "All notifications can be configured or disabled in Settings → Notifications."
|
||||
- "Cleaning up notifications implementation"
|
||||
- number: "Bugfixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Added more safety checks to nameplates"
|
||||
- "Removed a line in SyncshellUI potentially causing NullPointers"
|
||||
- "Additional safety checks in PlayerData.Factory"
|
||||
- name: "v1.12.2"
|
||||
tagline: "LightFinder fixes, Notifications overhaul"
|
||||
date: "October 12th 2025"
|
||||
versions:
|
||||
- number: "LightFinder"
|
||||
icon: ""
|
||||
items:
|
||||
- "Server-side improvements for LightFinder functionality."
|
||||
- "Command changed from '/light lightfinder' to '/light finder'."
|
||||
- "Option to enable LightFinder on connection (opt-in, refreshes every 3 hours)."
|
||||
- "LightFinder indicator can now be shown on the server info bar."
|
||||
- number: "Notifications"
|
||||
icon: ""
|
||||
items:
|
||||
- "Completely reworked notification system with new UI."
|
||||
- "Pair requests now show as notifications."
|
||||
- "Download progress shows as notifications."
|
||||
- "Customizable notification sounds, size, position, and duration."
|
||||
- "All notifications can be configured or disabled in Settings → Notifications."
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed nameplate alignment issues with LightFinder and icons."
|
||||
- "Icons now properly apply instead of swapping on choice."
|
||||
- "Updated Discord URL."
|
||||
- "File cache logic improvements."
|
||||
|
||||
- name: "v1.12.1"
|
||||
tagline: "LightFinder customization and download limiter"
|
||||
date: "October 8th 2025"
|
||||
versions:
|
||||
- number: "New Features"
|
||||
icon: ""
|
||||
items:
|
||||
- "LightFinder text can be modified to an icon with customizable positioning."
|
||||
- "Option to hide your own indicator or paired player indicators."
|
||||
- "Pair Download Limiter: Limit simultaneous downloads to 1-6 users to reduce network strain."
|
||||
- "Added '/light lightfinder' command to open LightFinder UI."
|
||||
- number: "Improvements"
|
||||
icon: ""
|
||||
items:
|
||||
- "Right-click menu option for Send Pair Request can be disabled."
|
||||
- "Syncshell finder improvements."
|
||||
- "Download limiter settings available in Settings → Transfers."
|
||||
|
||||
- name: "v1.12.0"
|
||||
tagline: "LightFinder - Major feature release"
|
||||
date: "October 5th 2025"
|
||||
versions:
|
||||
- number: "Major Features"
|
||||
icon: ""
|
||||
items:
|
||||
- "Introduced LightFinder: Optional feature inspired by FFXIV's Party Finder."
|
||||
- "Find fellow Lightless users and advertise your Syncshell to others."
|
||||
- "When enabled, you're visible to other LightFinder users for 3 hours."
|
||||
- "LightFinder tag displays above your nameplate when active."
|
||||
- "Receive pair requests directly in UI without exchanging UIDs."
|
||||
- "Syncshell Finder allows joining indexed Syncshells."
|
||||
- "[L] Send Pair Request added to player context menus."
|
||||
- number: "Vanity Features"
|
||||
icon: ""
|
||||
items:
|
||||
- "Supporters can now customize their name color in the Lightless UI."
|
||||
- "Color changes visible to all users."
|
||||
- number: "General Improvements"
|
||||
icon: ""
|
||||
items:
|
||||
- "Pairing nameplate color override can now override FC tags."
|
||||
- "Added .kdb as whitelisted filetype for uploads."
|
||||
- "Various UI fixes, updates, and improvements."
|
||||
|
||||
- name: "v1.11.12"
|
||||
tagline: "Syncshell grouping and performance options"
|
||||
date: "September 16th 2025"
|
||||
versions:
|
||||
- number: "New Features"
|
||||
icon: ""
|
||||
items:
|
||||
- "Ability to show grouped syncshells in main UI/all syncshells (default ON)."
|
||||
- "Transfer ownership button available in Admin Panel user list."
|
||||
- "Self-threshold warning now opens character analysis screen when clicked."
|
||||
- number: "Performance"
|
||||
icon: ""
|
||||
items:
|
||||
- "Auto-pause combat and auto-pause performance are now optional settings."
|
||||
- "Both options are auto-enabled by default - disable at your own risk."
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Reworked file caching to reduce errors for some users."
|
||||
- "Fixed bug where exiting PvP could desync some users."
|
||||
|
||||
- name: "v1.11.9"
|
||||
tagline: "File cache improvements"
|
||||
date: "September 13th 2025"
|
||||
versions:
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Identified and fixed potential file cache problems."
|
||||
- "Improved cache error handling and stability."
|
||||
|
||||
- name: "v1.11.8"
|
||||
tagline: "Hotfix - UI and exception handling"
|
||||
date: "September 12th 2025"
|
||||
versions:
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Attempted fix for NullReferenceException spam."
|
||||
- "Fixed additional UI edge cases preventing loading for some users."
|
||||
- "Fixed color bar UI issues."
|
||||
|
||||
- name: "v1.11.7"
|
||||
tagline: "Hotfix - UI loading and warnings"
|
||||
date: "September 12th 2025"
|
||||
versions:
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed UI not loading for some users."
|
||||
- "Self warnings now behind 'Warn on loading in players exceeding performance thresholds' setting."
|
||||
|
||||
- name: "v1.11.6"
|
||||
tagline: "Admin panel rework and new features"
|
||||
date: "September 11th 2025"
|
||||
versions:
|
||||
- number: "New Features"
|
||||
icon: ""
|
||||
items:
|
||||
- "Reworked Syncshell Admin Page with improved styling."
|
||||
- "Right-click on Server Top Bar button to disconnect from Lightless."
|
||||
- "Shift+Left click on Server Top Bar button to open settings."
|
||||
- "Added colors section in settings to change accent colors."
|
||||
- "Ability to pause syncing while in Instance/Duty."
|
||||
- "Functionality to create syncshell folders."
|
||||
- "Added self-threshold warning."
|
||||
- number: "Bug Fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed owners being visible in moderator list view."
|
||||
- "Removed Pin/Remove/Ban buttons on Owners when viewing as moderator."
|
||||
- "Fixed nameplate bug in PvP."
|
||||
- "Added 1 or 3 day options for inactive check."
|
||||
- "Fixed bug where some users could not see their own syncshell folders."
|
||||
@@ -1,68 +0,0 @@
|
||||
credits:
|
||||
- category: "Development Team"
|
||||
items:
|
||||
- name: "Abel"
|
||||
role: "Developer"
|
||||
- name: "Cake"
|
||||
role: "Developer"
|
||||
- name: "Celine"
|
||||
role: "Developer"
|
||||
- name: "Choco"
|
||||
role: "Developer"
|
||||
- name: "Kenny"
|
||||
role: "Developer"
|
||||
- name: "Zura"
|
||||
role: "Developer"
|
||||
- name: "Additional Contributors"
|
||||
role: "Community Contributors & Bug Reporters"
|
||||
|
||||
- category: "Moderation Team"
|
||||
items:
|
||||
- name: "Crow"
|
||||
role: "Moderator"
|
||||
- name: "Faith"
|
||||
role: "Moderator"
|
||||
- name: "Kiwiwiwi"
|
||||
role: "Moderator"
|
||||
- name: "Kruwu"
|
||||
role: "Moderator"
|
||||
- name: "Lexi"
|
||||
role: "Moderator"
|
||||
- name: "Maya"
|
||||
role: "Moderator"
|
||||
- name: "Metaknight"
|
||||
role: "Moderator"
|
||||
- name: "Minmoose"
|
||||
role: "Moderator"
|
||||
- name: "Nihal"
|
||||
role: "Moderator"
|
||||
- name: "Tani"
|
||||
role: "Moderator"
|
||||
|
||||
- category: "Plugin Integration & IPC Support"
|
||||
items:
|
||||
- name: "Penumbra Team"
|
||||
role: "Mod framework integration"
|
||||
- name: "Glamourer Team"
|
||||
role: "Customization system integration"
|
||||
- name: "Customize+ Team"
|
||||
role: "Body scaling integration"
|
||||
- name: "Simple Heels Team"
|
||||
role: "Height offset integration"
|
||||
- name: "Honorific Team"
|
||||
role: "Title system integration"
|
||||
- name: "Glyceri"
|
||||
role: "Moodles - Status effect integration"
|
||||
- name: "Glyceri"
|
||||
role: "PetNicknames - Pet naming integration"
|
||||
- name: "Minmoose"
|
||||
role: "Brio - GPose enhancement integration"
|
||||
|
||||
- category: "Special Thanks"
|
||||
items:
|
||||
- name: "Dalamud & XIVLauncher Teams"
|
||||
role: "Plugin framework and infrastructure"
|
||||
- name: "Community Supporters"
|
||||
role: "Testing, feedback, and financial support"
|
||||
- name: "Beta Testers"
|
||||
role: "Early testing and bug reporting"
|
||||
@@ -72,7 +72,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
while (_dalamudUtil.IsOnFrameworkThread && !token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(1, token).ConfigureAwait(false);
|
||||
await Task.Delay(1).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
RecalculateFileCacheSize(token);
|
||||
@@ -101,8 +101,8 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
|
||||
private readonly Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, WatcherChange> _watcherChanges = new Dictionary<string, WatcherChange>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new Dictionary<string, WatcherChange>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public void StopMonitoring()
|
||||
{
|
||||
@@ -115,8 +115,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
|
||||
public bool StorageisNTFS { get; private set; } = false;
|
||||
|
||||
public bool StorageIsBtrfs { get ; private set; } = false;
|
||||
|
||||
public void StartLightlessWatcher(string? lightlessPath)
|
||||
{
|
||||
LightlessWatcher?.Dispose();
|
||||
@@ -126,19 +124,10 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
Logger.LogWarning("Lightless file path is not set, cannot start the FSW for Lightless.");
|
||||
return;
|
||||
}
|
||||
var fsType = FileSystemHelper.GetFilesystemType(_configService.Current.CacheFolder, _dalamudUtil.IsWine);
|
||||
|
||||
if (fsType == FileSystemHelper.FilesystemType.NTFS && !_dalamudUtil.IsWine)
|
||||
{
|
||||
StorageisNTFS = true;
|
||||
Logger.LogInformation("Lightless Storage is on NTFS drive: {isNtfs}", StorageisNTFS);
|
||||
}
|
||||
|
||||
if (fsType == FileSystemHelper.FilesystemType.Btrfs)
|
||||
{
|
||||
StorageIsBtrfs = true;
|
||||
Logger.LogInformation("Lightless Storage is on BTRFS drive: {isBtrfs}", StorageIsBtrfs);
|
||||
}
|
||||
DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName);
|
||||
StorageisNTFS = string.Equals("NTFS", di.DriveFormat, StringComparison.OrdinalIgnoreCase);
|
||||
Logger.LogInformation("Lightless Storage is on NTFS drive: {isNtfs}", StorageisNTFS);
|
||||
|
||||
Logger.LogDebug("Initializing Lightless FSW on {path}", lightlessPath);
|
||||
LightlessWatcher = new()
|
||||
@@ -259,7 +248,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
|
||||
private CancellationTokenSource _penumbraFswCts = new();
|
||||
private CancellationTokenSource _lightlessFswCts = new();
|
||||
|
||||
public FileSystemWatcher? PenumbraWatcher { get; private set; }
|
||||
public FileSystemWatcher? LightlessWatcher { get; private set; }
|
||||
|
||||
@@ -404,140 +392,51 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
|
||||
public void RecalculateFileCacheSize(CancellationToken token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) ||
|
||||
!Directory.Exists(_configService.Current.CacheFolder))
|
||||
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder))
|
||||
{
|
||||
FileCacheSize = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
FileCacheSize = -1;
|
||||
bool isWine = _dalamudUtil?.IsWine ?? false;
|
||||
|
||||
DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName);
|
||||
try
|
||||
{
|
||||
var drive = DriveInfo.GetDrives()
|
||||
.FirstOrDefault(d => _configService.Current.CacheFolder
|
||||
.StartsWith(d.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (drive != null)
|
||||
FileCacheDriveFree = drive.AvailableFreeSpace;
|
||||
FileCacheDriveFree = di.AvailableFreeSpace;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", _configService.Current.CacheFolder);
|
||||
Logger.LogWarning(ex, "Could not determine drive size for Storage Folder {folder}", _configService.Current.CacheFolder);
|
||||
}
|
||||
|
||||
var files = Directory.EnumerateFiles(_configService.Current.CacheFolder)
|
||||
.Select(f => new FileInfo(f))
|
||||
.OrderBy(f => f.LastAccessTime)
|
||||
.ToList();
|
||||
|
||||
long totalSize = 0;
|
||||
|
||||
foreach (var f in files)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
long size = 0;
|
||||
|
||||
if (!isWine)
|
||||
{
|
||||
try
|
||||
{
|
||||
size = _fileCompactor.GetFileSizeOnDisk(f);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName);
|
||||
size = f.Length;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
size = f.Length;
|
||||
}
|
||||
|
||||
totalSize += size;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "Error getting size for {file}", f.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
FileCacheSize = totalSize;
|
||||
|
||||
if (Directory.Exists(_configService.Current.CacheFolder + "/downscaled"))
|
||||
{
|
||||
var filesDownscaled = Directory.EnumerateFiles(_configService.Current.CacheFolder + "/downscaled").Select(f => new FileInfo(f)).OrderBy(f => f.LastAccessTime).ToList();
|
||||
|
||||
long totalSizeDownscaled = 0;
|
||||
|
||||
foreach (var f in filesDownscaled)
|
||||
var files = Directory.EnumerateFiles(_configService.Current.CacheFolder).Select(f => new FileInfo(f))
|
||||
.OrderBy(f => f.LastAccessTime).ToList();
|
||||
FileCacheSize = files
|
||||
.Sum(f =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
long size = 0;
|
||||
|
||||
if (!isWine)
|
||||
{
|
||||
try
|
||||
{
|
||||
size = _fileCompactor.GetFileSizeOnDisk(f);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName);
|
||||
size = f.Length;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
size = f.Length;
|
||||
}
|
||||
|
||||
totalSizeDownscaled += size;
|
||||
return _fileCompactor.GetFileSizeOnDisk(f, StorageisNTFS);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
Logger.LogTrace(ex, "Error getting size for {file}", f.FullName);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
FileCacheSize = (totalSize + totalSizeDownscaled);
|
||||
}
|
||||
else
|
||||
{
|
||||
FileCacheSize = totalSize;
|
||||
}
|
||||
});
|
||||
|
||||
var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d);
|
||||
if (FileCacheSize < maxCacheInBytes)
|
||||
return;
|
||||
|
||||
if (FileCacheSize < maxCacheInBytes) return;
|
||||
|
||||
var maxCacheBuffer = maxCacheInBytes * 0.05d;
|
||||
|
||||
while (FileCacheSize > maxCacheInBytes - (long)maxCacheBuffer && files.Count > 0)
|
||||
while (FileCacheSize > maxCacheInBytes - (long)maxCacheBuffer)
|
||||
{
|
||||
var oldestFile = files[0];
|
||||
|
||||
try
|
||||
{
|
||||
long fileSize = oldestFile.Length;
|
||||
File.Delete(oldestFile.FullName);
|
||||
FileCacheSize -= fileSize;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogTrace(ex, "Failed to delete old file {file}", oldestFile.FullName);
|
||||
}
|
||||
|
||||
files.RemoveAt(0);
|
||||
FileCacheSize -= _fileCompactor.GetFileSizeOnDisk(oldestFile);
|
||||
File.Delete(oldestFile.FullName);
|
||||
files.Remove(oldestFile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,19 +456,12 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
// Disposing of file system watchers
|
||||
_scanCancellationTokenSource?.Cancel();
|
||||
PenumbraWatcher?.Dispose();
|
||||
LightlessWatcher?.Dispose();
|
||||
|
||||
// Disposing of cancellation token sources
|
||||
_scanCancellationTokenSource?.CancelDispose();
|
||||
_scanCancellationTokenSource?.Dispose();
|
||||
_penumbraFswCts?.CancelDispose();
|
||||
_penumbraFswCts?.Dispose();
|
||||
_lightlessFswCts?.CancelDispose();
|
||||
_lightlessFswCts?.Dispose();
|
||||
_periodicCalculationTokenSource?.CancelDispose();
|
||||
_periodicCalculationTokenSource?.Dispose();
|
||||
}
|
||||
|
||||
private void FullFileScan(CancellationToken ct)
|
||||
@@ -647,7 +539,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
|
||||
List<FileCacheEntity> entitiesToRemove = [];
|
||||
List<FileCacheEntity> entitiesToUpdate = [];
|
||||
Lock sync = new();
|
||||
object sync = new();
|
||||
Thread[] workerThreads = new Thread[threadCount];
|
||||
|
||||
ConcurrentQueue<FileCacheEntity> fileCaches = new(_fileDbManager.GetAllFileCaches());
|
||||
@@ -752,44 +644,44 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
|
||||
if (ct.IsCancellationRequested) return;
|
||||
|
||||
var newFiles = allScannedFiles.Where(c => !c.Value).Select(c => c.Key).ToList();
|
||||
foreach (var cachePath in newFiles)
|
||||
// scan new files
|
||||
if (allScannedFiles.Any(c => !c.Value))
|
||||
{
|
||||
if (ct.IsCancellationRequested) break;
|
||||
ProcessOne(cachePath);
|
||||
Interlocked.Increment(ref _currentFileProgress);
|
||||
}
|
||||
Parallel.ForEach(allScannedFiles.Where(c => !c.Value).Select(c => c.Key),
|
||||
new ParallelOptions()
|
||||
{
|
||||
MaxDegreeOfParallelism = threadCount,
|
||||
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;
|
||||
}
|
||||
|
||||
Logger.LogTrace("Scanner added {count} new files to db", newFiles.Count);
|
||||
if (ct.IsCancellationRequested) return;
|
||||
|
||||
void ProcessOne(string? cachePath)
|
||||
{
|
||||
if (_fileDbManager == null || _ipcManager?.Penumbra == null || cachePath == null)
|
||||
{
|
||||
Logger.LogTrace("Potential null in db: {isDbNull} penumbra: {isPenumbraNull} cachepath: {isPathNull}",
|
||||
_fileDbManager == null, _ipcManager?.Penumbra == null, cachePath == null);
|
||||
return;
|
||||
}
|
||||
if (!_ipcManager.Penumbra.APIAvailable)
|
||||
{
|
||||
Logger.LogWarning("Penumbra not available");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_ipcManager.Penumbra.APIAvailable)
|
||||
{
|
||||
Logger.LogWarning("Penumbra not available");
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
var entry = _fileDbManager.CreateFileEntry(cachePath);
|
||||
if (entry == null) _ = _fileDbManager.CreateCacheEntry(cachePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed adding {file}", cachePath);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var entry = _fileDbManager.CreateFileEntry(cachePath);
|
||||
if (entry == null) _ = _fileDbManager.CreateCacheEntry(cachePath);
|
||||
}
|
||||
catch (IOException ioex)
|
||||
{
|
||||
Logger.LogDebug(ioex, "File busy or locked: {file}", cachePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed adding {file}", cachePath);
|
||||
}
|
||||
Interlocked.Increment(ref _currentFileProgress);
|
||||
});
|
||||
|
||||
Logger.LogTrace("Scanner added {notScanned} new files to db", allScannedFiles.Count(c => !c.Value));
|
||||
}
|
||||
|
||||
Logger.LogDebug("Scan complete");
|
||||
|
||||
@@ -16,9 +16,6 @@ public sealed class FileCacheManager : IHostedService
|
||||
public const string CachePrefix = "{cache}";
|
||||
public const string CsvSplit = "|";
|
||||
public const string PenumbraPrefix = "{penumbra}";
|
||||
private const int FileCacheVersion = 1;
|
||||
private const string FileCacheVersionHeaderPrefix = "#lightless-file-cache-version:";
|
||||
private readonly SemaphoreSlim _fileWriteSemaphore = new(1, 1);
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly string _csvPath;
|
||||
@@ -28,7 +25,6 @@ public sealed class FileCacheManager : IHostedService
|
||||
private readonly Lock _fileWriteLock = new();
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly ILogger<FileCacheManager> _logger;
|
||||
private bool _csvHeaderEnsured;
|
||||
public string CacheFolder => _configService.Current.CacheFolder;
|
||||
|
||||
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator)
|
||||
@@ -42,8 +38,11 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
private string CsvBakPath => _csvPath + ".bak";
|
||||
|
||||
private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal)
|
||||
private static string NormalizeSeparators(string path)
|
||||
{
|
||||
return path.Replace("/", "\\", StringComparison.Ordinal)
|
||||
.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string NormalizePrefixedPathKey(string prefixedPath)
|
||||
{
|
||||
@@ -55,62 +54,6 @@ public sealed class FileCacheManager : IHostedService
|
||||
return NormalizeSeparators(prefixedPath).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool TryBuildPrefixedPath(string path, string? baseDirectory, string prefix, out string prefixedPath, out int matchedLength)
|
||||
{
|
||||
prefixedPath = string.Empty;
|
||||
matchedLength = 0;
|
||||
|
||||
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(baseDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedPath = NormalizeSeparators(path).ToLowerInvariant();
|
||||
var normalizedBase = NormalizeSeparators(baseDirectory).TrimEnd('\\').ToLowerInvariant();
|
||||
|
||||
if (!normalizedPath.StartsWith(normalizedBase, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalizedPath.Length > normalizedBase.Length)
|
||||
{
|
||||
if (normalizedPath[normalizedBase.Length] != '\\')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
prefixedPath = prefix + normalizedPath.Substring(normalizedBase.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
prefixedPath = prefix;
|
||||
}
|
||||
|
||||
prefixedPath = prefixedPath.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
matchedLength = normalizedBase.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BuildVersionHeader() => $"{FileCacheVersionHeaderPrefix}{FileCacheVersion}";
|
||||
|
||||
private static bool TryParseVersionHeader(string? line, out int version)
|
||||
{
|
||||
version = 0;
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!line.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var versionSpan = line.AsSpan(FileCacheVersionHeaderPrefix.Length);
|
||||
return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version);
|
||||
}
|
||||
|
||||
private string NormalizeToPrefixedPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||||
@@ -123,21 +66,27 @@ public sealed class FileCacheManager : IHostedService
|
||||
return NormalizePrefixedPathKey(normalized);
|
||||
}
|
||||
|
||||
string? chosenPrefixed = null;
|
||||
var chosenLength = -1;
|
||||
|
||||
if (TryBuildPrefixedPath(normalized, _ipcManager.Penumbra.ModDirectory, PenumbraPrefix, out var penumbraPrefixed, out var penumbraMatch))
|
||||
var penumbraDir = _ipcManager.Penumbra.ModDirectory;
|
||||
if (!string.IsNullOrEmpty(penumbraDir))
|
||||
{
|
||||
chosenPrefixed = penumbraPrefixed;
|
||||
chosenLength = penumbraMatch;
|
||||
var normalizedPenumbra = NormalizeSeparators(penumbraDir);
|
||||
var replacement = normalizedPenumbra.EndsWith("\\", StringComparison.Ordinal)
|
||||
? PenumbraPrefix + "\\"
|
||||
: PenumbraPrefix;
|
||||
normalized = normalized.Replace(normalizedPenumbra, replacement, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch) && cacheMatch > chosenLength)
|
||||
var cacheFolder = _configService.Current.CacheFolder;
|
||||
if (!string.IsNullOrEmpty(cacheFolder))
|
||||
{
|
||||
chosenPrefixed = cachePrefixed;
|
||||
var normalizedCache = NormalizeSeparators(cacheFolder);
|
||||
var replacement = normalizedCache.EndsWith("\\", StringComparison.Ordinal)
|
||||
? CachePrefix + "\\"
|
||||
: CachePrefix;
|
||||
normalized = normalized.Replace(normalizedCache, replacement, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return NormalizePrefixedPathKey(chosenPrefixed ?? normalized);
|
||||
return NormalizePrefixedPathKey(normalized);
|
||||
}
|
||||
|
||||
public FileCacheEntity? CreateCacheEntry(string path)
|
||||
@@ -145,9 +94,7 @@ public sealed class FileCacheManager : IHostedService
|
||||
FileInfo fi = new(path);
|
||||
if (!fi.Exists) return null;
|
||||
_logger.LogTrace("Creating cache entry for {path}", path);
|
||||
var cacheFolder = _configService.Current.CacheFolder;
|
||||
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
||||
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
||||
return CreateFileEntity(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix, fi);
|
||||
}
|
||||
|
||||
public FileCacheEntity? CreateFileEntry(string path)
|
||||
@@ -155,141 +102,80 @@ public sealed class FileCacheManager : IHostedService
|
||||
FileInfo fi = new(path);
|
||||
if (!fi.Exists) return null;
|
||||
_logger.LogTrace("Creating file entry for {path}", path);
|
||||
var modDirectory = _ipcManager.Penumbra.ModDirectory;
|
||||
if (string.IsNullOrEmpty(modDirectory)) return null;
|
||||
return CreateFileEntity(modDirectory, PenumbraPrefix, fi);
|
||||
return CreateFileEntity(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix, fi);
|
||||
}
|
||||
|
||||
private FileCacheEntity? CreateFileEntity(string directory, string prefix, FileInfo fi)
|
||||
{
|
||||
if (!TryBuildPrefixedPath(fi.FullName, directory, prefix, out var prefixedPath, out _))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var fullName = fi.FullName.ToLowerInvariant();
|
||||
if (!fullName.Contains(directory, StringComparison.Ordinal)) return null;
|
||||
string prefixedPath = fullName.Replace(directory, prefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
return CreateFileCacheEntity(fi, prefixedPath);
|
||||
}
|
||||
|
||||
public List<FileCacheEntity> GetAllFileCaches() => [.. _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null))];
|
||||
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null)).ToList();
|
||||
|
||||
public List<FileCacheEntity> GetAllFileCachesByHash(string hash, bool ignoreCacheEntries = false, bool validate = true)
|
||||
{
|
||||
var output = new List<FileCacheEntity>();
|
||||
|
||||
if (!_fileCaches.TryGetValue(hash, out var fileCacheEntities))
|
||||
return output;
|
||||
|
||||
foreach (var fileCache in fileCacheEntities.Values
|
||||
.Where(c => !ignoreCacheEntries || !c.IsCacheEntry))
|
||||
List<FileCacheEntity> output = [];
|
||||
if (_fileCaches.TryGetValue(hash, out var fileCacheEntities))
|
||||
{
|
||||
if (!validate)
|
||||
foreach (var fileCache in fileCacheEntities.Values.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList())
|
||||
{
|
||||
output.Add(fileCache);
|
||||
continue;
|
||||
}
|
||||
|
||||
var validated = GetValidatedFileCache(fileCache);
|
||||
if (validated != null)
|
||||
output.Add(validated);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public async Task<List<FileCacheEntity>> GetAllFileCachesByHashAsync(string hash, bool ignoreCacheEntries = false, bool validate = true,CancellationToken token = default)
|
||||
{
|
||||
var output = new List<FileCacheEntity>();
|
||||
|
||||
if (!_fileCaches.TryGetValue(hash, out var fileCacheEntities))
|
||||
return output;
|
||||
|
||||
foreach (var fileCache in fileCacheEntities.Values.Where(c => !ignoreCacheEntries || !c.IsCacheEntry))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (!validate)
|
||||
{
|
||||
output.Add(fileCache);
|
||||
}
|
||||
else
|
||||
{
|
||||
var validated = await GetValidatedFileCacheAsync(fileCache, token).ConfigureAwait(false);
|
||||
|
||||
if (validated != null)
|
||||
output.Add(validated);
|
||||
if (!validate)
|
||||
{
|
||||
output.Add(fileCache);
|
||||
}
|
||||
else
|
||||
{
|
||||
var validated = GetValidatedFileCache(fileCache);
|
||||
if (validated != null)
|
||||
{
|
||||
output.Add(validated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public async Task<List<FileCacheEntity>> ValidateLocalIntegrity(IProgress<(int completed, int total, FileCacheEntity current)> progress, CancellationToken cancellationToken)
|
||||
public Task<List<FileCacheEntity>> ValidateLocalIntegrity(IProgress<(int, int, FileCacheEntity)> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
_lightlessMediator.Publish(new HaltScanMessage(nameof(ValidateLocalIntegrity)));
|
||||
_logger.LogInformation("Validating local storage");
|
||||
|
||||
var cacheEntries = _fileCaches.Values
|
||||
.SelectMany(v => v.Values)
|
||||
.Where(v => v.IsCacheEntry)
|
||||
.ToList();
|
||||
|
||||
int total = cacheEntries.Count;
|
||||
int processed = 0;
|
||||
var brokenEntities = new ConcurrentBag<FileCacheEntity>();
|
||||
|
||||
_logger.LogInformation("Checking {count} cache entries...", total);
|
||||
|
||||
await Parallel.ForEachAsync(cacheEntries, new ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = Environment.ProcessorCount,
|
||||
CancellationToken = cancellationToken
|
||||
},
|
||||
async (fileCache, token) =>
|
||||
var cacheEntries = _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null)).Where(v => v.IsCacheEntry).ToList();
|
||||
List<FileCacheEntity> brokenEntities = [];
|
||||
int i = 0;
|
||||
foreach (var fileCache in cacheEntries)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) break;
|
||||
|
||||
_logger.LogInformation("Validating {file}", fileCache.ResolvedFilepath);
|
||||
|
||||
progress.Report((i, cacheEntries.Count, fileCache));
|
||||
i++;
|
||||
if (!File.Exists(fileCache.ResolvedFilepath))
|
||||
{
|
||||
brokenEntities.Add(fileCache);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int current = Interlocked.Increment(ref processed);
|
||||
if (current % 10 == 0)
|
||||
progress.Report((current, total, fileCache));
|
||||
|
||||
if (!File.Exists(fileCache.ResolvedFilepath))
|
||||
{
|
||||
brokenEntities.Add(fileCache);
|
||||
return;
|
||||
}
|
||||
|
||||
var algo = Crypto.DetectAlgo(fileCache.Hash);
|
||||
string computedHash;
|
||||
try
|
||||
{
|
||||
computedHash = await Crypto.ComputeFileHashAsync(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error hashing {file}", fileCache.ResolvedFilepath);
|
||||
brokenEntities.Add(fileCache);
|
||||
return;
|
||||
}
|
||||
|
||||
var computedHash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
|
||||
if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Hash mismatch: {file} (got {computedHash}, expected {expected} : hash {hash})",
|
||||
fileCache.ResolvedFilepath, computedHash, fileCache.Hash, algo);
|
||||
|
||||
_logger.LogInformation("Failed to validate {file}, got hash {computedHash}, expected hash {hash}", fileCache.ResolvedFilepath, computedHash, fileCache.Hash);
|
||||
brokenEntities.Add(fileCache);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError("Validation got cancelled for {file}", fileCache.ResolvedFilepath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected error validating {file}", fileCache.ResolvedFilepath);
|
||||
_logger.LogWarning(e, "Error during validation of {file}", fileCache.ResolvedFilepath);
|
||||
brokenEntities.Add(fileCache);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var brokenEntity in brokenEntities)
|
||||
{
|
||||
@@ -301,14 +187,12 @@ public sealed class FileCacheManager : IHostedService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to delete invalid cache file {file}", brokenEntity.ResolvedFilepath);
|
||||
_logger.LogWarning(ex, "Could not delete {file}", brokenEntity.ResolvedFilepath);
|
||||
}
|
||||
}
|
||||
|
||||
_lightlessMediator.Publish(new ResumeScanMessage(nameof(ValidateLocalIntegrity)));
|
||||
_logger.LogInformation("Validation complete. Found {count} invalid entries.", brokenEntities.Count);
|
||||
|
||||
return [.. brokenEntities];
|
||||
return Task.FromResult(brokenEntities);
|
||||
}
|
||||
|
||||
public string GetCacheFilePath(string hash, string extension)
|
||||
@@ -455,7 +339,7 @@ public sealed class FileCacheManager : IHostedService
|
||||
var fi = new FileInfo(fileCache.ResolvedFilepath);
|
||||
fileCache.Size = fi.Length;
|
||||
fileCache.CompressedSize = null;
|
||||
fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1);
|
||||
fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath);
|
||||
fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
RemoveHashedFile(oldHash, prefixedPath);
|
||||
@@ -483,7 +367,6 @@ public sealed class FileCacheManager : IHostedService
|
||||
lock (_fileWriteLock)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine(BuildVersionHeader());
|
||||
foreach (var entry in _fileCaches.Values.SelectMany(k => k.Values).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
sb.AppendLine(entry.CsvEntry);
|
||||
@@ -506,104 +389,6 @@ public sealed class FileCacheManager : IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WriteOutFullCsvAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _fileWriteSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(BuildVersionHeader());
|
||||
|
||||
foreach (var entry in _fileCaches.Values
|
||||
.SelectMany(k => k.Values)
|
||||
.OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
sb.AppendLine(entry.CsvEntry);
|
||||
}
|
||||
|
||||
if (File.Exists(_csvPath))
|
||||
{
|
||||
File.Copy(_csvPath, CsvBakPath, overwrite: true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(_csvPath, sb.ToString(), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
File.Delete(CsvBakPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await File.WriteAllTextAsync(CsvBakPath, sb.ToString(), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fileWriteSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureCsvHeaderLocked()
|
||||
{
|
||||
if (!File.Exists(_csvPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string[] existingLines = File.ReadAllLines(_csvPath);
|
||||
if (existingLines.Length > 0 && TryParseVersionHeader(existingLines[0], out var existingVersion) && existingVersion == FileCacheVersion)
|
||||
{
|
||||
_csvHeaderEnsured = true;
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder rebuilt = new();
|
||||
rebuilt.AppendLine(BuildVersionHeader());
|
||||
foreach (var line in existingLines)
|
||||
{
|
||||
if (TryParseVersionHeader(line, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
{
|
||||
rebuilt.AppendLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(_csvPath, rebuilt.ToString());
|
||||
_csvHeaderEnsured = true;
|
||||
}
|
||||
|
||||
private void EnsureCsvHeaderLockedCached()
|
||||
{
|
||||
if (_csvHeaderEnsured)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureCsvHeaderLocked();
|
||||
_csvHeaderEnsured = true;
|
||||
}
|
||||
|
||||
private void BackupUnsupportedCache(string suffix)
|
||||
{
|
||||
var sanitizedSuffix = string.IsNullOrWhiteSpace(suffix) ? "unsupported" : $"{suffix}.unsupported";
|
||||
var backupPath = _csvPath + "." + sanitizedSuffix;
|
||||
|
||||
try
|
||||
{
|
||||
File.Move(_csvPath, backupPath, overwrite: true);
|
||||
_logger.LogWarning("Backed up unsupported file cache to {path}", backupPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to back up unsupported file cache to {path}", backupPath);
|
||||
}
|
||||
}
|
||||
|
||||
internal FileCacheEntity MigrateFileHashToExtension(FileCacheEntity fileCache, string ext)
|
||||
{
|
||||
try
|
||||
@@ -636,22 +421,13 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
|
||||
{
|
||||
hash ??= Crypto.ComputeFileHash(fileInfo.FullName, Crypto.HashAlgo.Sha1);
|
||||
hash ??= Crypto.GetFileHash(fileInfo.FullName);
|
||||
var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileInfo.Length);
|
||||
entity = ReplacePathPrefixes(entity);
|
||||
AddHashedFile(entity);
|
||||
lock (_fileWriteLock)
|
||||
{
|
||||
if (!File.Exists(_csvPath))
|
||||
{
|
||||
File.WriteAllLines(_csvPath, [BuildVersionHeader(), entity.CsvEntry]);
|
||||
_csvHeaderEnsured = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureCsvHeaderLockedCached();
|
||||
File.AppendAllLines(_csvPath, [entity.CsvEntry]);
|
||||
}
|
||||
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
||||
}
|
||||
var result = GetFileCacheByPath(fileInfo.FullName);
|
||||
_logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null));
|
||||
@@ -661,17 +437,11 @@ public sealed class FileCacheManager : IHostedService
|
||||
private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache)
|
||||
{
|
||||
var resultingFileCache = ReplacePathPrefixes(fileCache);
|
||||
//_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath);
|
||||
resultingFileCache = Validate(resultingFileCache);
|
||||
return resultingFileCache;
|
||||
}
|
||||
|
||||
private async Task<FileCacheEntity?> GetValidatedFileCacheAsync(FileCacheEntity fileCache, CancellationToken token = default)
|
||||
{
|
||||
var resultingFileCache = ReplacePathPrefixes(fileCache);
|
||||
resultingFileCache = await ValidateAsync(resultingFileCache, token).ConfigureAwait(false);
|
||||
return resultingFileCache;
|
||||
}
|
||||
|
||||
private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache)
|
||||
{
|
||||
if (fileCache.PrefixedFilePath.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -694,7 +464,6 @@ public sealed class FileCacheManager : IHostedService
|
||||
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
var file = new FileInfo(fileCache.ResolvedFilepath);
|
||||
if (!file.Exists)
|
||||
{
|
||||
@@ -702,8 +471,7 @@ public sealed class FileCacheManager : IHostedService
|
||||
return null;
|
||||
}
|
||||
|
||||
var lastWriteTicks = file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
||||
if (!string.Equals(lastWriteTicks, fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
|
||||
if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
|
||||
{
|
||||
UpdateHashedFile(fileCache);
|
||||
}
|
||||
@@ -711,34 +479,7 @@ public sealed class FileCacheManager : IHostedService
|
||||
return fileCache;
|
||||
}
|
||||
|
||||
private async Task<FileCacheEntity?> ValidateAsync(FileCacheEntity fileCache, CancellationToken token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileCache.ResolvedFilepath))
|
||||
{
|
||||
_logger.LogWarning("FileCacheEntity has empty ResolvedFilepath for hash {hash}, prefixed path {prefixed}", fileCache.Hash, fileCache.PrefixedFilePath);
|
||||
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var file = new FileInfo(fileCache.ResolvedFilepath);
|
||||
if (!file.Exists)
|
||||
{
|
||||
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal))
|
||||
{
|
||||
UpdateHashedFile(fileCache);
|
||||
}
|
||||
|
||||
return fileCache;
|
||||
}, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Starting FileCacheManager");
|
||||
|
||||
@@ -789,14 +530,14 @@ public sealed class FileCacheManager : IHostedService
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Attempting to read {csvPath}", _csvPath);
|
||||
entries = await File.ReadAllLinesAsync(_csvPath, cancellationToken).ConfigureAwait(false);
|
||||
entries = File.ReadAllLines(_csvPath);
|
||||
success = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
attempts++;
|
||||
_logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath);
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
Task.Delay(100, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -805,124 +546,62 @@ public sealed class FileCacheManager : IHostedService
|
||||
_logger.LogWarning("Could not load entries from {path}, continuing with empty file cache", _csvPath);
|
||||
}
|
||||
|
||||
bool rewriteRequired = false;
|
||||
bool parseEntries = entries.Length > 0;
|
||||
int startIndex = 0;
|
||||
_logger.LogInformation("Found {amount} files in {path}", entries.Length, _csvPath);
|
||||
|
||||
if (entries.Length > 0)
|
||||
{
|
||||
var headerLine = entries[0];
|
||||
var hasHeader = !string.IsNullOrEmpty(headerLine) &&
|
||||
headerLine.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (hasHeader)
|
||||
{
|
||||
if (!TryParseVersionHeader(headerLine, out var parsedVersion))
|
||||
{
|
||||
_logger.LogWarning("Failed to parse file cache version header \"{header}\". Backing up existing cache.", headerLine);
|
||||
BackupUnsupportedCache("invalid-version");
|
||||
parseEntries = false;
|
||||
rewriteRequired = true;
|
||||
entries = [];
|
||||
}
|
||||
else if (parsedVersion != FileCacheVersion)
|
||||
{
|
||||
_logger.LogWarning("Unsupported file cache version {version} detected (expected {expected}). Backing up existing cache.", parsedVersion, FileCacheVersion);
|
||||
BackupUnsupportedCache($"v{parsedVersion}");
|
||||
parseEntries = false;
|
||||
rewriteRequired = true;
|
||||
entries = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
startIndex = 1;
|
||||
}
|
||||
}
|
||||
else if (entries.Length > 0)
|
||||
{
|
||||
_logger.LogInformation("File cache missing version header, scheduling rewrite.");
|
||||
rewriteRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
var totalEntries = Math.Max(0, entries.Length - startIndex);
|
||||
Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (parseEntries && totalEntries > 0)
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
_logger.LogInformation("Found {amount} files in {path}", totalEntries, _csvPath);
|
||||
|
||||
for (var index = startIndex; index < entries.Length; index++)
|
||||
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
|
||||
try
|
||||
{
|
||||
var entry = entries[index];
|
||||
if (string.IsNullOrWhiteSpace(entry))
|
||||
var hash = splittedEntry[0];
|
||||
if (hash.Length != 40) throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
|
||||
var path = splittedEntry[1];
|
||||
var time = splittedEntry[2];
|
||||
|
||||
if (processedFiles.ContainsKey(path))
|
||||
{
|
||||
_logger.LogWarning("Already processed {file}, ignoring", path);
|
||||
continue;
|
||||
}
|
||||
|
||||
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
|
||||
try
|
||||
processedFiles.Add(path, value: true);
|
||||
|
||||
long size = -1;
|
||||
long compressed = -1;
|
||||
if (splittedEntry.Length > 3)
|
||||
{
|
||||
var hash = splittedEntry[0];
|
||||
if (hash.Length != 40)
|
||||
throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
|
||||
var path = splittedEntry[1];
|
||||
var time = splittedEntry[2];
|
||||
|
||||
if (processedFiles.ContainsKey(path))
|
||||
if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
|
||||
{
|
||||
_logger.LogWarning("Already processed {file}, ignoring", path);
|
||||
continue;
|
||||
size = result;
|
||||
}
|
||||
|
||||
processedFiles.Add(path, value: true);
|
||||
|
||||
long size = -1;
|
||||
long compressed = -1;
|
||||
if (splittedEntry.Length > 3)
|
||||
if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
|
||||
{
|
||||
if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
|
||||
{
|
||||
size = result;
|
||||
}
|
||||
if (splittedEntry.Length > 4 &&
|
||||
long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
|
||||
{
|
||||
compressed = resultCompressed;
|
||||
}
|
||||
compressed = resultCompressed;
|
||||
}
|
||||
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
|
||||
}
|
||||
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
|
||||
}
|
||||
|
||||
if (processedFiles.Count != totalEntries)
|
||||
catch (Exception ex)
|
||||
{
|
||||
rewriteRequired = true;
|
||||
_logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
|
||||
}
|
||||
}
|
||||
else if (!parseEntries && entries.Length > 0)
|
||||
{
|
||||
_logger.LogInformation("Skipping existing file cache entries due to incompatible version.");
|
||||
}
|
||||
|
||||
if (rewriteRequired)
|
||||
if (processedFiles.Count != entries.Length)
|
||||
{
|
||||
await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false);
|
||||
WriteOutFullCsv();
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Started FileCacheManager");
|
||||
_lightlessMediator.Publish(new FileCacheInitializedMessage());
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
WriteOutFullCsv();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,5 +5,4 @@ public enum FileState
|
||||
Valid,
|
||||
RequireUpdate,
|
||||
RequireDeletion,
|
||||
RequireRehash
|
||||
}
|
||||
@@ -3,17 +3,11 @@ using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
using LightlessSync.PlayerData.Data;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
|
||||
namespace LightlessSync.FileCache;
|
||||
|
||||
@@ -23,29 +17,21 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
private readonly HashSet<string> _cachedHandledPaths = new(StringComparer.Ordinal);
|
||||
private readonly TransientConfigService _configurationService;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
private readonly object _ownedHandlerLock = new();
|
||||
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"];
|
||||
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
|
||||
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
||||
private readonly Dictionary<nint, GameObjectHandler> _ownedHandlers = new();
|
||||
private ConcurrentDictionary<nint, ObjectKind> _cachedFrameAddresses = new();
|
||||
private ConcurrentDictionary<IntPtr, ObjectKind> _cachedFrameAddresses = [];
|
||||
private ConcurrentDictionary<ObjectKind, HashSet<string>>? _semiTransientResources = null;
|
||||
private uint _lastClassJobId = uint.MaxValue;
|
||||
public bool IsTransientRecording { get; private set; } = false;
|
||||
|
||||
public TransientResourceManager(ILogger<TransientResourceManager> logger, TransientConfigService configurationService,
|
||||
DalamudUtilService dalamudUtil, LightlessMediator mediator, ActorObjectService actorObjectService, GameObjectHandlerFactory gameObjectHandlerFactory) : base(logger, mediator)
|
||||
DalamudUtilService dalamudUtil, LightlessMediator mediator) : base(logger, mediator)
|
||||
{
|
||||
_configurationService = configurationService;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_actorObjectService = actorObjectService;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
|
||||
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
|
||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor));
|
||||
Mediator.Subscribe<PenumbraModSettingChangedMessage>(this, (_) => Manager_PenumbraModSettingChanged());
|
||||
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, (_) => DalamudUtil_FrameworkUpdate());
|
||||
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
||||
@@ -58,11 +44,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
if (!msg.OwnedObject) return;
|
||||
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
||||
});
|
||||
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
{
|
||||
HandleActorTracked(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
private TransientConfig.TransientPlayerConfig PlayerConfig
|
||||
@@ -142,21 +123,12 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> transientResources;
|
||||
lock (resources)
|
||||
{
|
||||
transientResources = resources.ToList();
|
||||
}
|
||||
|
||||
var transientResources = resources.ToList();
|
||||
Logger.LogDebug("Persisting {count} transient resources", transientResources.Count);
|
||||
List<string> newlyAddedGamePaths;
|
||||
lock (semiTransientResources)
|
||||
List<string> newlyAddedGamePaths = resources.Except(semiTransientResources, StringComparer.Ordinal).ToList();
|
||||
foreach (var gamePath in transientResources)
|
||||
{
|
||||
newlyAddedGamePaths = transientResources.Except(semiTransientResources, StringComparer.Ordinal).ToList();
|
||||
foreach (var gamePath in transientResources)
|
||||
{
|
||||
semiTransientResources.Add(gamePath);
|
||||
}
|
||||
semiTransientResources.Add(gamePath);
|
||||
}
|
||||
|
||||
bool saveConfig = false;
|
||||
@@ -189,10 +161,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
_configurationService.Save();
|
||||
}
|
||||
|
||||
lock (resources)
|
||||
{
|
||||
resources.Clear();
|
||||
}
|
||||
TransientResources[objectKind].Clear();
|
||||
}
|
||||
|
||||
public void RemoveTransientResource(ObjectKind objectKind, string path)
|
||||
@@ -272,46 +241,16 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
|
||||
TransientResources.Clear();
|
||||
SemiTransientResources.Clear();
|
||||
|
||||
lock (_ownedHandlerLock)
|
||||
{
|
||||
foreach (var handler in _ownedHandlers.Values)
|
||||
{
|
||||
handler.Dispose();
|
||||
}
|
||||
_ownedHandlers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void DalamudUtil_FrameworkUpdate()
|
||||
{
|
||||
_cachedFrameAddresses = new(_playerRelatedPointers.Where(k => k.Address != nint.Zero).ToDictionary(c => c.Address, c => c.ObjectKind));
|
||||
lock (_cacheAdditionLock)
|
||||
{
|
||||
_cachedHandledPaths.Clear();
|
||||
}
|
||||
|
||||
var activeDescriptors = new Dictionary<nint, ObjectKind>();
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
{
|
||||
if (TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||
{
|
||||
activeDescriptors[descriptor.Address] = resolvedKind;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var address in _cachedFrameAddresses.Keys.ToList())
|
||||
{
|
||||
if (!activeDescriptors.ContainsKey(address))
|
||||
{
|
||||
_cachedFrameAddresses.TryRemove(address, out _);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var descriptor in activeDescriptors)
|
||||
{
|
||||
_cachedFrameAddresses[descriptor.Key] = descriptor.Value;
|
||||
}
|
||||
|
||||
if (_lastClassJobId != _dalamudUtil.ClassJobId)
|
||||
{
|
||||
_lastClassJobId = _dalamudUtil.ClassJobId;
|
||||
@@ -320,15 +259,16 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
value?.Clear();
|
||||
}
|
||||
|
||||
// reload config for current new classjob
|
||||
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
||||
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
||||
SemiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
|
||||
}
|
||||
|
||||
foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>())
|
||||
foreach (var kind in Enum.GetValues(typeof(ObjectKind)))
|
||||
{
|
||||
if (!_cachedFrameAddresses.Any(k => k.Value == kind) && TransientResources.Remove(kind, out _))
|
||||
if (!_cachedFrameAddresses.Any(k => k.Value == (ObjectKind)kind) && TransientResources.Remove((ObjectKind)kind, out _))
|
||||
{
|
||||
Logger.LogDebug("Object not present anymore: {kind}", kind.ToString());
|
||||
}
|
||||
@@ -352,116 +292,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
_semiTransientResources = null;
|
||||
}
|
||||
|
||||
private static bool TryResolveObjectKind(ActorObjectService.ActorDescriptor descriptor, out ObjectKind resolvedKind)
|
||||
{
|
||||
if (descriptor.OwnedKind is ObjectKind ownedKind)
|
||||
{
|
||||
resolvedKind = ownedKind;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
{
|
||||
resolvedKind = ObjectKind.Player;
|
||||
return true;
|
||||
}
|
||||
|
||||
resolvedKind = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
if (!TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||
return;
|
||||
|
||||
if (Logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
Logger.LogDebug("ActorObject tracked: {kind} addr={address:X} name={name}", resolvedKind, descriptor.Address, descriptor.Name);
|
||||
}
|
||||
|
||||
_cachedFrameAddresses[descriptor.Address] = resolvedKind;
|
||||
|
||||
if (descriptor.OwnedKind is not ObjectKind ownedKind)
|
||||
return;
|
||||
|
||||
lock (_ownedHandlerLock)
|
||||
{
|
||||
if (_ownedHandlers.ContainsKey(descriptor.Address))
|
||||
return;
|
||||
|
||||
_ = CreateOwnedHandlerAsync(descriptor, ownedKind);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleActorUntracked(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
if (Logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
var kindLabel = descriptor.OwnedKind?.ToString()
|
||||
?? (descriptor.ObjectKind == DalamudObjectKind.Player ? ObjectKind.Player.ToString() : "<none>");
|
||||
Logger.LogDebug("ActorObject untracked: addr={address:X} name={name} kind={kind}", descriptor.Address, descriptor.Name, kindLabel);
|
||||
}
|
||||
|
||||
_cachedFrameAddresses.TryRemove(descriptor.Address, out _);
|
||||
|
||||
if (descriptor.OwnedKind is not ObjectKind)
|
||||
return;
|
||||
|
||||
lock (_ownedHandlerLock)
|
||||
{
|
||||
if (_ownedHandlers.Remove(descriptor.Address, out var handler))
|
||||
{
|
||||
handler.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateOwnedHandlerAsync(ActorObjectService.ActorDescriptor descriptor, ObjectKind kind)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handler = await _gameObjectHandlerFactory.Create(
|
||||
kind,
|
||||
() =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(descriptor.HashedContentId) &&
|
||||
_actorObjectService.TryGetValidatedActorByHash(descriptor.HashedContentId, out var current) &&
|
||||
current.OwnedKind == kind)
|
||||
{
|
||||
return current.Address;
|
||||
}
|
||||
|
||||
return descriptor.Address;
|
||||
},
|
||||
true).ConfigureAwait(false);
|
||||
|
||||
if (handler.Address == IntPtr.Zero)
|
||||
{
|
||||
handler.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_ownedHandlerLock)
|
||||
{
|
||||
if (!_cachedFrameAddresses.ContainsKey(descriptor.Address))
|
||||
{
|
||||
Logger.LogDebug("ActorObject handler discarded (stale): addr={address:X}", descriptor.Address);
|
||||
handler.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
_ownedHandlers[descriptor.Address] = handler;
|
||||
}
|
||||
|
||||
Logger.LogDebug("ActorObject handler created: {kind} addr={address:X}", kind, descriptor.Address);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to create owned handler for {kind} at {address:X}", kind, descriptor.Address);
|
||||
}
|
||||
}
|
||||
|
||||
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
|
||||
{
|
||||
var gamePath = msg.GamePath.ToLowerInvariant();
|
||||
@@ -553,30 +383,21 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
|
||||
private void SendTransients(nint gameObject, ObjectKind objectKind)
|
||||
{
|
||||
_sendTransientCts.Cancel();
|
||||
_sendTransientCts = new();
|
||||
var token = _sendTransientCts.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
_sendTransientCts?.Cancel();
|
||||
_sendTransientCts?.Dispose();
|
||||
_sendTransientCts = new();
|
||||
var token = _sendTransientCts.Token;
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false);
|
||||
foreach (var kvp in TransientResources)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false);
|
||||
|
||||
if (TransientResources.TryGetValue(objectKind, out var values) && values.Any())
|
||||
{
|
||||
Logger.LogTrace("Sending Transients for {kind}", objectKind);
|
||||
Mediator.Publish(new TransientResourceChangedMessage(gameObject));
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
catch (System.OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,7 @@ internal sealed class DalamudLogger : ILogger
|
||||
_hasModifiedGameFiles = hasModifiedGameFiles;
|
||||
}
|
||||
|
||||
IDisposable? ILogger.BeginScope<TState>(TState state)
|
||||
{
|
||||
return default!;
|
||||
}
|
||||
public IDisposable BeginScope<TState>(TState state) => default!;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Linq;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Framework;
|
||||
|
||||
public enum IpcConnectionState
|
||||
{
|
||||
Unknown = 0,
|
||||
MissingPlugin = 1,
|
||||
VersionMismatch = 2,
|
||||
PluginDisabled = 3,
|
||||
NotReady = 4,
|
||||
Available = 5,
|
||||
Error = 6,
|
||||
}
|
||||
|
||||
public sealed record IpcServiceDescriptor(string InternalName, string DisplayName, Version MinimumVersion)
|
||||
{
|
||||
public override string ToString()
|
||||
=> $"{DisplayName} (>= {MinimumVersion})";
|
||||
}
|
||||
|
||||
public interface IIpcService : IDisposable
|
||||
{
|
||||
IpcServiceDescriptor Descriptor { get; }
|
||||
IpcConnectionState State { get; }
|
||||
IDalamudPluginInterface PluginInterface { get; }
|
||||
bool APIAvailable { get; }
|
||||
void CheckAPI();
|
||||
}
|
||||
|
||||
public interface IIpcInterop : IDisposable
|
||||
{
|
||||
string Name { get; }
|
||||
void OnConnectionStateChanged(IpcConnectionState state);
|
||||
}
|
||||
|
||||
public abstract class IpcInteropBase : IIpcInterop
|
||||
{
|
||||
protected IpcInteropBase(ILogger logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
protected IpcConnectionState State { get; private set; } = IpcConnectionState.Unknown;
|
||||
|
||||
protected bool IsAvailable => State == IpcConnectionState.Available;
|
||||
|
||||
public abstract string Name { get; }
|
||||
|
||||
public void OnConnectionStateChanged(IpcConnectionState state)
|
||||
{
|
||||
if (State == state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var previous = State;
|
||||
State = state;
|
||||
HandleStateChange(previous, state);
|
||||
}
|
||||
|
||||
protected abstract void HandleStateChange(IpcConnectionState previous, IpcConnectionState current);
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class IpcServiceBase : DisposableMediatorSubscriberBase, IIpcService
|
||||
{
|
||||
private readonly List<IIpcInterop> _interops = new();
|
||||
|
||||
protected IpcServiceBase(
|
||||
ILogger logger,
|
||||
LightlessMediator mediator,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
IpcServiceDescriptor descriptor) : base(logger, mediator)
|
||||
{
|
||||
PluginInterface = pluginInterface;
|
||||
Descriptor = descriptor;
|
||||
}
|
||||
|
||||
protected IDalamudPluginInterface PluginInterface { get; }
|
||||
|
||||
IDalamudPluginInterface IIpcService.PluginInterface => PluginInterface;
|
||||
|
||||
protected IpcServiceDescriptor Descriptor { get; }
|
||||
|
||||
IpcServiceDescriptor IIpcService.Descriptor => Descriptor;
|
||||
|
||||
public IpcConnectionState State { get; private set; } = IpcConnectionState.Unknown;
|
||||
|
||||
public bool APIAvailable => State == IpcConnectionState.Available;
|
||||
|
||||
public virtual void CheckAPI()
|
||||
{
|
||||
var newState = EvaluateState();
|
||||
UpdateState(newState);
|
||||
}
|
||||
|
||||
protected virtual IpcConnectionState EvaluateState()
|
||||
{
|
||||
try
|
||||
{
|
||||
var plugin = PluginInterface.InstalledPlugins
|
||||
.Where(p => string.Equals(p.InternalName, Descriptor.InternalName, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(p => p.IsLoaded)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (plugin == null)
|
||||
{
|
||||
return IpcConnectionState.MissingPlugin;
|
||||
}
|
||||
|
||||
if (plugin.Version < Descriptor.MinimumVersion)
|
||||
{
|
||||
return IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
|
||||
if (!IsPluginEnabled(plugin))
|
||||
{
|
||||
return IpcConnectionState.PluginDisabled;
|
||||
}
|
||||
|
||||
if (!IsPluginReady())
|
||||
{
|
||||
return IpcConnectionState.NotReady;
|
||||
}
|
||||
|
||||
return IpcConnectionState.Available;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed to evaluate IPC state for {Service}", Descriptor.DisplayName);
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool IsPluginEnabled(IExposedPlugin plugin)
|
||||
=> plugin.IsLoaded;
|
||||
|
||||
protected virtual bool IsPluginReady()
|
||||
=> true;
|
||||
|
||||
protected TInterop RegisterInterop<TInterop>(TInterop interop)
|
||||
where TInterop : IIpcInterop
|
||||
{
|
||||
_interops.Add(interop);
|
||||
interop.OnConnectionStateChanged(State);
|
||||
return interop;
|
||||
}
|
||||
|
||||
private void UpdateState(IpcConnectionState newState)
|
||||
{
|
||||
if (State == newState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var previous = State;
|
||||
State = newState;
|
||||
OnConnectionStateChanged(previous, newState);
|
||||
|
||||
foreach (var interop in _interops)
|
||||
{
|
||||
interop.OnConnectionStateChanged(newState);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
Logger.LogTrace("{Service} IPC state transitioned from {Previous} to {Current}", Descriptor.DisplayName, previous, current);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = _interops.Count - 1; i >= 0; --i)
|
||||
{
|
||||
_interops[i].Dispose();
|
||||
}
|
||||
|
||||
_interops.Clear();
|
||||
}
|
||||
}
|
||||
7
LightlessSync/Interop/Ipc/IIpcCaller.cs
Normal file
7
LightlessSync/Interop/Ipc/IIpcCaller.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public interface IIpcCaller : IDisposable
|
||||
{
|
||||
bool APIAvailable { get; }
|
||||
void CheckAPI();
|
||||
}
|
||||
@@ -1,63 +1,69 @@
|
||||
using Brio.API;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerBrio : IpcServiceBase
|
||||
public sealed class IpcCallerBrio : IIpcCaller
|
||||
{
|
||||
private static readonly IpcServiceDescriptor BrioDescriptor = new("Brio", "Brio", new Version(3, 0, 0, 0));
|
||||
|
||||
private readonly ILogger<IpcCallerBrio> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly ICallGateSubscriber<(int, int)> _brioApiVersion;
|
||||
|
||||
private readonly ApiVersion _apiVersion;
|
||||
private readonly ICallGateSubscriber<bool, bool, bool, Task<IGameObject>> _brioSpawnActorAsync;
|
||||
private readonly ICallGateSubscriber<IGameObject, bool> _brioDespawnActor;
|
||||
private readonly ICallGateSubscriber<IGameObject, Vector3?, Quaternion?, Vector3?, bool, bool> _brioSetModelTransform;
|
||||
private readonly ICallGateSubscriber<IGameObject, (Vector3?, Quaternion?, Vector3?)> _brioGetModelTransform;
|
||||
private readonly ICallGateSubscriber<IGameObject, string> _brioGetPoseAsJson;
|
||||
private readonly ICallGateSubscriber<IGameObject, string, bool, bool> _brioSetPoseFromJson;
|
||||
private readonly ICallGateSubscriber<IGameObject, bool> _brioFreezeActor;
|
||||
private readonly ICallGateSubscriber<bool> _brioFreezePhysics;
|
||||
|
||||
private readonly SpawnActor _spawnActor;
|
||||
private readonly DespawnActor _despawnActor;
|
||||
private readonly SetModelTransform _setModelTransform;
|
||||
private readonly GetModelTransform _getModelTransform;
|
||||
|
||||
private readonly GetPoseAsJson _getPoseAsJson;
|
||||
private readonly LoadPoseFromJson _setPoseFromJson;
|
||||
|
||||
private readonly FreezeActor _freezeActor;
|
||||
private readonly FreezePhysics _freezePhysics;
|
||||
public bool APIAvailable { get; private set; }
|
||||
|
||||
public IpcCallerBrio(ILogger<IpcCallerBrio> logger, IDalamudPluginInterface dalamudPluginInterface,
|
||||
DalamudUtilService dalamudUtilService, LightlessMediator mediator) : base(logger, mediator, dalamudPluginInterface, BrioDescriptor)
|
||||
DalamudUtilService dalamudUtilService)
|
||||
{
|
||||
_logger = logger;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
|
||||
_apiVersion = new ApiVersion(dalamudPluginInterface);
|
||||
_spawnActor = new SpawnActor(dalamudPluginInterface);
|
||||
_despawnActor = new DespawnActor(dalamudPluginInterface);
|
||||
|
||||
_setModelTransform = new SetModelTransform(dalamudPluginInterface);
|
||||
_getModelTransform = new GetModelTransform(dalamudPluginInterface);
|
||||
|
||||
_getPoseAsJson = new GetPoseAsJson(dalamudPluginInterface);
|
||||
_setPoseFromJson = new LoadPoseFromJson(dalamudPluginInterface);
|
||||
|
||||
_freezeActor = new FreezeActor(dalamudPluginInterface);
|
||||
_freezePhysics = new FreezePhysics(dalamudPluginInterface);
|
||||
_brioApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("Brio.ApiVersion");
|
||||
_brioSpawnActorAsync = dalamudPluginInterface.GetIpcSubscriber<bool, bool, bool, Task<IGameObject>>("Brio.Actor.SpawnExAsync");
|
||||
_brioDespawnActor = dalamudPluginInterface.GetIpcSubscriber<IGameObject, bool>("Brio.Actor.Despawn");
|
||||
_brioSetModelTransform = dalamudPluginInterface.GetIpcSubscriber<IGameObject, Vector3?, Quaternion?, Vector3?, bool, bool>("Brio.Actor.SetModelTransform");
|
||||
_brioGetModelTransform = dalamudPluginInterface.GetIpcSubscriber<IGameObject, (Vector3?, Quaternion?, Vector3?)>("Brio.Actor.GetModelTransform");
|
||||
_brioGetPoseAsJson = dalamudPluginInterface.GetIpcSubscriber<IGameObject, string>("Brio.Actor.Pose.GetPoseAsJson");
|
||||
_brioSetPoseFromJson = dalamudPluginInterface.GetIpcSubscriber<IGameObject, string, bool, bool>("Brio.Actor.Pose.LoadFromJson");
|
||||
_brioFreezeActor = dalamudPluginInterface.GetIpcSubscriber<IGameObject, bool>("Brio.Actor.Freeze");
|
||||
_brioFreezePhysics = dalamudPluginInterface.GetIpcSubscriber<bool>("Brio.FreezePhysics");
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = _brioApiVersion.InvokeFunc();
|
||||
APIAvailable = (version.Item1 == 2 && version.Item2 >= 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IGameObject?> SpawnActorAsync()
|
||||
{
|
||||
if (!APIAvailable) return null;
|
||||
_logger.LogDebug("Spawning Brio Actor");
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _spawnActor.Invoke(Brio.API.Enums.SpawnFlags.Default, true)).ConfigureAwait(false);
|
||||
return await _brioSpawnActorAsync.InvokeFunc(false, false, true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> DespawnActorAsync(nint address)
|
||||
@@ -66,7 +72,7 @@ public sealed class IpcCallerBrio : IpcServiceBase
|
||||
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||
if (gameObject == null) return false;
|
||||
_logger.LogDebug("Despawning Brio Actor {actor}", gameObject.Name.TextValue);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _despawnActor.Invoke(gameObject)).ConfigureAwait(false);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioDespawnActor.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> ApplyTransformAsync(nint address, WorldData data)
|
||||
@@ -76,7 +82,7 @@ public sealed class IpcCallerBrio : IpcServiceBase
|
||||
if (gameObject == null) return false;
|
||||
_logger.LogDebug("Applying Transform to Actor {actor}", gameObject.Name.TextValue);
|
||||
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _setModelTransform.Invoke(gameObject,
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetModelTransform.InvokeFunc(gameObject,
|
||||
new Vector3(data.PositionX, data.PositionY, data.PositionZ),
|
||||
new Quaternion(data.RotationX, data.RotationY, data.RotationZ, data.RotationW),
|
||||
new Vector3(data.ScaleX, data.ScaleY, data.ScaleZ), false)).ConfigureAwait(false);
|
||||
@@ -87,7 +93,8 @@ public sealed class IpcCallerBrio : IpcServiceBase
|
||||
if (!APIAvailable) return default;
|
||||
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||
if (gameObject == null) return default;
|
||||
var data = await _dalamudUtilService.RunOnFrameworkThread(() => _getModelTransform.Invoke(gameObject)).ConfigureAwait(false);
|
||||
var data = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetModelTransform.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||
//_logger.LogDebug("Getting Transform from Actor {actor}", gameObject.Name.TextValue);
|
||||
|
||||
return new WorldData()
|
||||
{
|
||||
@@ -111,7 +118,7 @@ public sealed class IpcCallerBrio : IpcServiceBase
|
||||
if (gameObject == null) return null;
|
||||
_logger.LogDebug("Getting Pose from Actor {actor}", gameObject.Name.TextValue);
|
||||
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _getPoseAsJson.Invoke(gameObject)).ConfigureAwait(false);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> SetPoseAsync(nint address, string pose)
|
||||
@@ -122,41 +129,18 @@ public sealed class IpcCallerBrio : IpcServiceBase
|
||||
_logger.LogDebug("Setting Pose to Actor {actor}", gameObject.Name.TextValue);
|
||||
|
||||
var applicablePose = JsonNode.Parse(pose)!;
|
||||
var currentPose = await _dalamudUtilService.RunOnFrameworkThread(() => _getPoseAsJson.Invoke(gameObject)).ConfigureAwait(false);
|
||||
var currentPose = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||
applicablePose["ModelDifference"] = JsonNode.Parse(JsonNode.Parse(currentPose)!["ModelDifference"]!.ToJsonString());
|
||||
|
||||
await _dalamudUtilService.RunOnFrameworkThread(() =>
|
||||
{
|
||||
_freezeActor.Invoke(gameObject);
|
||||
_freezePhysics.Invoke();
|
||||
_brioFreezeActor.InvokeFunc(gameObject);
|
||||
_brioFreezePhysics.InvokeFunc();
|
||||
}).ConfigureAwait(false);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _setPoseFromJson.Invoke(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetPoseFromJson.InvokeFunc(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
public void Dispose()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var version = _apiVersion.Invoke();
|
||||
return version.Item1 == 3 && version.Item2 >= 0
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to query Brio IPC version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Dalamud.Utility;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -10,10 +9,8 @@ using System.Text;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerCustomize : IpcServiceBase
|
||||
public sealed class IpcCallerCustomize : IIpcCaller
|
||||
{
|
||||
private static readonly IpcServiceDescriptor CustomizeDescriptor = new("CustomizePlus", "Customize+", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ICallGateSubscriber<(int, int)> _customizePlusApiVersion;
|
||||
private readonly ICallGateSubscriber<ushort, (int, Guid?)> _customizePlusGetActiveProfile;
|
||||
private readonly ICallGateSubscriber<Guid, (int, string?)> _customizePlusGetProfileById;
|
||||
@@ -26,7 +23,7 @@ public sealed class IpcCallerCustomize : IpcServiceBase
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
|
||||
public IpcCallerCustomize(ILogger<IpcCallerCustomize> logger, IDalamudPluginInterface dalamudPluginInterface,
|
||||
DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator) : base(logger, lightlessMediator, dalamudPluginInterface, CustomizeDescriptor)
|
||||
DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator)
|
||||
{
|
||||
_customizePlusApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("CustomizePlus.General.GetApiVersion");
|
||||
_customizePlusGetActiveProfile = dalamudPluginInterface.GetIpcSubscriber<ushort, (int, Guid?)>("CustomizePlus.Profile.GetActiveProfileIdOnCharacter");
|
||||
@@ -44,6 +41,8 @@ public sealed class IpcCallerCustomize : IpcServiceBase
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public async Task RevertAsync(nint character)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
@@ -114,25 +113,16 @@ public sealed class IpcCallerCustomize : IpcServiceBase
|
||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale));
|
||||
}
|
||||
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
public void CheckAPI()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var version = _customizePlusApiVersion.InvokeFunc();
|
||||
return version.Item1 == 6 && version.Item2 >= 0
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
APIAvailable = (version.Item1 == 6 && version.Item2 >= 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed to query Customize+ API version");
|
||||
return IpcConnectionState.Error;
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,14 +132,8 @@ public sealed class IpcCallerCustomize : IpcServiceBase
|
||||
_lightlessMediator.Publish(new CustomizePlusMessage(obj?.Address ?? null));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
public void Dispose()
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_customizePlusOnScaleUpdate.Unsubscribe(OnCustomizePlusScaleChange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using Dalamud.Plugin;
|
||||
using Glamourer.Api.Helpers;
|
||||
using Glamourer.Api.IpcSubscribers;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
@@ -11,9 +10,8 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerGlamourer : IpcServiceBase
|
||||
public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcCaller
|
||||
{
|
||||
private static readonly IpcServiceDescriptor GlamourerDescriptor = new("Glamourer", "Glamourer", new Version(1, 3, 0, 10));
|
||||
private readonly ILogger<IpcCallerGlamourer> _logger;
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
@@ -33,7 +31,7 @@ public sealed class IpcCallerGlamourer : IpcServiceBase
|
||||
private readonly uint LockCode = 0x6D617265;
|
||||
|
||||
public IpcCallerGlamourer(ILogger<IpcCallerGlamourer> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator,
|
||||
RedrawManager redrawManager) : base(logger, lightlessMediator, pi, GlamourerDescriptor)
|
||||
RedrawManager redrawManager) : base(logger, lightlessMediator)
|
||||
{
|
||||
_glamourerApiVersions = new ApiVersion(pi);
|
||||
_glamourerGetAllCustomization = new GetStateBase64(pi);
|
||||
@@ -64,6 +62,47 @@ public sealed class IpcCallerGlamourer : IpcServiceBase
|
||||
_glamourerStateChanged?.Dispose();
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; }
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
bool apiAvailable = false;
|
||||
try
|
||||
{
|
||||
bool versionValid = (_pi.InstalledPlugins
|
||||
.FirstOrDefault(p => string.Equals(p.InternalName, "Glamourer", StringComparison.OrdinalIgnoreCase))
|
||||
?.Version ?? new Version(0, 0, 0, 0)) >= new Version(1, 3, 0, 10);
|
||||
try
|
||||
{
|
||||
var version = _glamourerApiVersions.Invoke();
|
||||
if (version is { Major: 1, Minor: >= 1 } && versionValid)
|
||||
{
|
||||
apiAvailable = true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
_shownGlamourerUnavailable = _shownGlamourerUnavailable && !apiAvailable;
|
||||
|
||||
APIAvailable = apiAvailable;
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = apiAvailable;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!apiAvailable && !_shownGlamourerUnavailable)
|
||||
{
|
||||
_shownGlamourerUnavailable = true;
|
||||
_lightlessMediator.Publish(new NotificationMessage("Glamourer inactive", "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Lightless. If you just updated Glamourer, ignore this message.",
|
||||
NotificationType.Error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ApplyAllAsync(ILogger logger, GameObjectHandler handler, string? customization, Guid applicationId, CancellationToken token, bool fireAndForget = false)
|
||||
{
|
||||
if (!APIAvailable || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return;
|
||||
@@ -171,49 +210,6 @@ public sealed class IpcCallerGlamourer : IpcServiceBase
|
||||
}
|
||||
}
|
||||
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var version = _glamourerApiVersions.Invoke();
|
||||
return version is { Major: 1, Minor: >= 1 }
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to query Glamourer API version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
base.OnConnectionStateChanged(previous, current);
|
||||
|
||||
if (current == IpcConnectionState.Available)
|
||||
{
|
||||
_shownGlamourerUnavailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_shownGlamourerUnavailable || current == IpcConnectionState.Unknown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_shownGlamourerUnavailable = true;
|
||||
_lightlessMediator.Publish(new NotificationMessage("Glamourer inactive",
|
||||
"Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Lightless. If you just updated Glamourer, ignore this message.",
|
||||
NotificationType.Error));
|
||||
}
|
||||
|
||||
private void GlamourerChanged(nint address)
|
||||
{
|
||||
_lightlessMediator.Publish(new GlamourerChangedMessage(address));
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerHeels : IpcServiceBase
|
||||
public sealed class IpcCallerHeels : IIpcCaller
|
||||
{
|
||||
private static readonly IpcServiceDescriptor HeelsDescriptor = new("SimpleHeels", "Simple Heels", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ILogger<IpcCallerHeels> _logger;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
@@ -21,7 +18,6 @@ public sealed class IpcCallerHeels : IpcServiceBase
|
||||
private readonly ICallGateSubscriber<int, object?> _heelsUnregisterPlayer;
|
||||
|
||||
public IpcCallerHeels(ILogger<IpcCallerHeels> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator)
|
||||
: base(logger, lightlessMediator, pi, HeelsDescriptor)
|
||||
{
|
||||
_logger = logger;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
@@ -36,26 +32,8 @@ public sealed class IpcCallerHeels : IpcServiceBase
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 1 }
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to query SimpleHeels API version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
private void HeelsOffsetChange(string offset)
|
||||
{
|
||||
@@ -96,14 +74,20 @@ public sealed class IpcCallerHeels : IpcServiceBase
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
public void CheckAPI()
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
try
|
||||
{
|
||||
return;
|
||||
APIAvailable = _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 1 };
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -9,10 +8,8 @@ using System.Text;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerHonorific : IpcServiceBase
|
||||
public sealed class IpcCallerHonorific : IIpcCaller
|
||||
{
|
||||
private static readonly IpcServiceDescriptor HonorificDescriptor = new("Honorific", "Honorific", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ICallGateSubscriber<(uint major, uint minor)> _honorificApiVersion;
|
||||
private readonly ICallGateSubscriber<int, object> _honorificClearCharacterTitle;
|
||||
private readonly ICallGateSubscriber<object> _honorificDisposing;
|
||||
@@ -25,7 +22,7 @@ public sealed class IpcCallerHonorific : IpcServiceBase
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
|
||||
public IpcCallerHonorific(ILogger<IpcCallerHonorific> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, HonorificDescriptor)
|
||||
LightlessMediator lightlessMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
@@ -44,14 +41,23 @@ public sealed class IpcCallerHonorific : IpcServiceBase
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
try
|
||||
{
|
||||
APIAvailable = _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 1 };
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_honorificLocalCharacterTitleChanged.Unsubscribe(OnHonorificLocalCharacterTitleChanged);
|
||||
_honorificDisposing.Unsubscribe(OnHonorificDisposing);
|
||||
_honorificReady.Unsubscribe(OnHonorificReady);
|
||||
@@ -107,27 +113,6 @@ public sealed class IpcCallerHonorific : IpcServiceBase
|
||||
}
|
||||
}
|
||||
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 1 }
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to query Honorific API version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHonorificDisposing()
|
||||
{
|
||||
_lightlessMediator.Publish(new HonorificMessage(string.Empty));
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
public sealed class IpcCallerMoodles : IIpcCaller
|
||||
{
|
||||
private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ICallGateSubscriber<int> _moodlesApiVersion;
|
||||
private readonly ICallGateSubscriber<IPlayerCharacter, object> _moodlesOnChange;
|
||||
private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus;
|
||||
@@ -22,7 +19,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
|
||||
public IpcCallerMoodles(ILogger<IpcCallerMoodles> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, MoodlesDescriptor)
|
||||
LightlessMediator lightlessMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
@@ -44,14 +41,22 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
_lightlessMediator.Publish(new MoodlesMessage(character.Address));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
try
|
||||
{
|
||||
APIAvailable = _moodlesApiVersion.InvokeFunc() == 3;
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_moodlesOnChange.Unsubscribe(OnMoodlesChange);
|
||||
}
|
||||
|
||||
@@ -96,25 +101,4 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
_logger.LogWarning(e, "Could not Set Moodles Status");
|
||||
}
|
||||
}
|
||||
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return _moodlesApiVersion.InvokeFunc() == 3
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to query Moodles API version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,205 +1,146 @@
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Interop.Ipc.Penumbra;
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerPenumbra : IpcServiceBase
|
||||
public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCaller
|
||||
{
|
||||
private static readonly IpcServiceDescriptor PenumbraDescriptor = new("Penumbra", "Penumbra", new Version(1, 2, 0, 22));
|
||||
|
||||
private readonly PenumbraCollections _collections;
|
||||
private readonly PenumbraResource _resources;
|
||||
private readonly PenumbraRedraw _redraw;
|
||||
private readonly PenumbraTexture _textures;
|
||||
|
||||
private readonly GetEnabledState _penumbraEnabled;
|
||||
private readonly GetModDirectory _penumbraGetModDirectory;
|
||||
private readonly EventSubscriber _penumbraInit;
|
||||
private readonly EventSubscriber _penumbraDispose;
|
||||
private readonly EventSubscriber<ModSettingChange, Guid, string, bool> _penumbraModSettingChanged;
|
||||
|
||||
private bool _shownPenumbraUnavailable;
|
||||
private string? _modDirectory;
|
||||
|
||||
public IpcCallerPenumbra(
|
||||
ILogger<IpcCallerPenumbra> logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator,
|
||||
RedrawManager redrawManager,
|
||||
ActorObjectService actorObjectService) : base(logger, mediator, pluginInterface, PenumbraDescriptor)
|
||||
{
|
||||
_penumbraEnabled = new GetEnabledState(pluginInterface);
|
||||
_penumbraGetModDirectory = new GetModDirectory(pluginInterface);
|
||||
_penumbraInit = Initialized.Subscriber(pluginInterface, HandlePenumbraInitialized);
|
||||
_penumbraDispose = Disposed.Subscriber(pluginInterface, HandlePenumbraDisposed);
|
||||
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pluginInterface, HandlePenumbraModSettingChanged);
|
||||
|
||||
_collections = RegisterInterop(new PenumbraCollections(logger, pluginInterface, dalamudUtil, mediator));
|
||||
_resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator, actorObjectService));
|
||||
_redraw = RegisterInterop(new PenumbraRedraw(logger, pluginInterface, dalamudUtil, mediator, redrawManager));
|
||||
_textures = RegisterInterop(new PenumbraTexture(logger, pluginInterface, dalamudUtil, mediator, _redraw));
|
||||
|
||||
SubscribeMediatorEvents();
|
||||
|
||||
CheckAPI();
|
||||
CheckModDirectory();
|
||||
}
|
||||
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly RedrawManager _redrawManager;
|
||||
private bool _shownPenumbraUnavailable = false;
|
||||
private string? _penumbraModDirectory;
|
||||
public string? ModDirectory
|
||||
{
|
||||
get => _modDirectory;
|
||||
get => _penumbraModDirectory;
|
||||
private set
|
||||
{
|
||||
if (string.Equals(_modDirectory, value, StringComparison.Ordinal))
|
||||
if (!string.Equals(_penumbraModDirectory, value, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
_penumbraModDirectory = value;
|
||||
_lightlessMediator.Publish(new PenumbraDirectoryChangedMessage(_penumbraModDirectory));
|
||||
}
|
||||
|
||||
_modDirectory = value;
|
||||
Mediator.Publish(new PenumbraDirectoryChangedMessage(_modDirectory));
|
||||
}
|
||||
}
|
||||
|
||||
public Task AssignTemporaryCollectionAsync(ILogger logger, Guid collectionId, int objectIndex)
|
||||
=> _collections.AssignTemporaryCollectionAsync(logger, collectionId, objectIndex);
|
||||
private readonly ConcurrentDictionary<IntPtr, bool> _penumbraRedrawRequests = new();
|
||||
|
||||
public Task<Guid> CreateTemporaryCollectionAsync(ILogger logger, string uid)
|
||||
=> _collections.CreateTemporaryCollectionAsync(logger, uid);
|
||||
private readonly EventSubscriber _penumbraDispose;
|
||||
private readonly EventSubscriber<nint, string, string> _penumbraGameObjectResourcePathResolved;
|
||||
private readonly EventSubscriber _penumbraInit;
|
||||
private readonly EventSubscriber<ModSettingChange, Guid, string, bool> _penumbraModSettingChanged;
|
||||
private readonly EventSubscriber<nint, int> _penumbraObjectIsRedrawn;
|
||||
|
||||
public Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collectionId)
|
||||
=> _collections.RemoveTemporaryCollectionAsync(logger, applicationId, collectionId);
|
||||
private readonly AddTemporaryMod _penumbraAddTemporaryMod;
|
||||
private readonly AssignTemporaryCollection _penumbraAssignTemporaryCollection;
|
||||
private readonly ConvertTextureFile _penumbraConvertTextureFile;
|
||||
private readonly CreateTemporaryCollection _penumbraCreateNamedTemporaryCollection;
|
||||
private readonly GetEnabledState _penumbraEnabled;
|
||||
private readonly GetPlayerMetaManipulations _penumbraGetMetaManipulations;
|
||||
private readonly RedrawObject _penumbraRedraw;
|
||||
private readonly DeleteTemporaryCollection _penumbraRemoveTemporaryCollection;
|
||||
private readonly RemoveTemporaryMod _penumbraRemoveTemporaryMod;
|
||||
private readonly GetModDirectory _penumbraResolveModDir;
|
||||
private readonly ResolvePlayerPathsAsync _penumbraResolvePaths;
|
||||
private readonly GetGameObjectResourcePaths _penumbraResourcePaths;
|
||||
|
||||
public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
|
||||
=> _collections.SetTemporaryModsAsync(logger, applicationId, collectionId, modPaths);
|
||||
public IpcCallerPenumbra(ILogger<IpcCallerPenumbra> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
LightlessMediator lightlessMediator, RedrawManager redrawManager) : base(logger, lightlessMediator)
|
||||
{
|
||||
_pi = pi;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_redrawManager = redrawManager;
|
||||
_penumbraInit = Initialized.Subscriber(pi, PenumbraInit);
|
||||
_penumbraDispose = Disposed.Subscriber(pi, PenumbraDispose);
|
||||
_penumbraResolveModDir = new GetModDirectory(pi);
|
||||
_penumbraRedraw = new RedrawObject(pi);
|
||||
_penumbraObjectIsRedrawn = GameObjectRedrawn.Subscriber(pi, RedrawEvent);
|
||||
_penumbraGetMetaManipulations = new GetPlayerMetaManipulations(pi);
|
||||
_penumbraRemoveTemporaryMod = new RemoveTemporaryMod(pi);
|
||||
_penumbraAddTemporaryMod = new AddTemporaryMod(pi);
|
||||
_penumbraCreateNamedTemporaryCollection = new CreateTemporaryCollection(pi);
|
||||
_penumbraRemoveTemporaryCollection = new DeleteTemporaryCollection(pi);
|
||||
_penumbraAssignTemporaryCollection = new AssignTemporaryCollection(pi);
|
||||
_penumbraResolvePaths = new ResolvePlayerPathsAsync(pi);
|
||||
_penumbraEnabled = new GetEnabledState(pi);
|
||||
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pi, (change, arg1, arg, b) =>
|
||||
{
|
||||
if (change == ModSettingChange.EnableState)
|
||||
_lightlessMediator.Publish(new PenumbraModSettingChangedMessage());
|
||||
});
|
||||
_penumbraConvertTextureFile = new ConvertTextureFile(pi);
|
||||
_penumbraResourcePaths = new GetGameObjectResourcePaths(pi);
|
||||
|
||||
public Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData)
|
||||
=> _collections.SetManipulationDataAsync(logger, applicationId, collectionId, manipulationData);
|
||||
_penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded);
|
||||
|
||||
public Task<Dictionary<string, HashSet<string>>?> GetCharacterData(ILogger logger, GameObjectHandler handler)
|
||||
=> _resources.GetCharacterDataAsync(logger, handler);
|
||||
CheckAPI();
|
||||
CheckModDirectory();
|
||||
|
||||
public string GetMetaManipulations()
|
||||
=> _resources.GetMetaManipulations();
|
||||
Mediator.Subscribe<PenumbraRedrawCharacterMessage>(this, (msg) =>
|
||||
{
|
||||
_penumbraRedraw.Invoke(msg.Character.ObjectIndex, RedrawType.AfterGPose);
|
||||
});
|
||||
|
||||
public Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
|
||||
=> _resources.ResolvePathsAsync(forward, reverse);
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, (msg) => _shownPenumbraUnavailable = false);
|
||||
}
|
||||
|
||||
public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||
=> _redraw.RedrawAsync(logger, handler, applicationId, token);
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public Task ConvertTextureFiles(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
||||
=> _textures.ConvertTextureFilesAsync(logger, jobs, progress, token);
|
||||
|
||||
public Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token)
|
||||
=> _textures.ConvertTextureFileDirectAsync(job, token);
|
||||
public void CheckAPI()
|
||||
{
|
||||
bool penumbraAvailable = false;
|
||||
try
|
||||
{
|
||||
var penumbraVersion = (_pi.InstalledPlugins
|
||||
.FirstOrDefault(p => string.Equals(p.InternalName, "Penumbra", StringComparison.OrdinalIgnoreCase))
|
||||
?.Version ?? new Version(0, 0, 0, 0));
|
||||
penumbraAvailable = penumbraVersion >= new Version(1, 2, 0, 22);
|
||||
try
|
||||
{
|
||||
penumbraAvailable &= _penumbraEnabled.Invoke();
|
||||
}
|
||||
catch
|
||||
{
|
||||
penumbraAvailable = false;
|
||||
}
|
||||
_shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable;
|
||||
APIAvailable = penumbraAvailable;
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = penumbraAvailable;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!penumbraAvailable && !_shownPenumbraUnavailable)
|
||||
{
|
||||
_shownPenumbraUnavailable = true;
|
||||
_lightlessMediator.Publish(new NotificationMessage("Penumbra inactive",
|
||||
"Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Lightless. If you just updated Penumbra, ignore this message.",
|
||||
NotificationType.Error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CheckModDirectory()
|
||||
{
|
||||
if (!APIAvailable)
|
||||
{
|
||||
ModDirectory = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
else
|
||||
{
|
||||
ModDirectory = _penumbraGetModDirectory.Invoke().ToLowerInvariant();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to resolve Penumbra mod directory");
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool IsPluginEnabled(IExposedPlugin plugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _penumbraEnabled.Invoke();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
base.OnConnectionStateChanged(previous, current);
|
||||
|
||||
if (current == IpcConnectionState.Available)
|
||||
{
|
||||
_shownPenumbraUnavailable = false;
|
||||
if (string.IsNullOrEmpty(ModDirectory))
|
||||
{
|
||||
CheckModDirectory();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ModDirectory = string.Empty;
|
||||
_redraw.CancelPendingRedraws();
|
||||
|
||||
if (_shownPenumbraUnavailable || current == IpcConnectionState.Unknown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_shownPenumbraUnavailable = true;
|
||||
Mediator.Publish(new NotificationMessage(
|
||||
"Penumbra inactive",
|
||||
"Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Lightless. If you just updated Penumbra, ignore this message.",
|
||||
NotificationType.Error));
|
||||
}
|
||||
|
||||
private void SubscribeMediatorEvents()
|
||||
{
|
||||
Mediator.Subscribe<PenumbraRedrawCharacterMessage>(this, msg =>
|
||||
{
|
||||
_redraw.RequestImmediateRedraw(msg.Character.ObjectIndex, RedrawType.AfterGPose);
|
||||
});
|
||||
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, _ => _shownPenumbraUnavailable = false);
|
||||
|
||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => _resources.TrackActor(msg.Descriptor.Address));
|
||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => _resources.UntrackActor(msg.Descriptor.Address));
|
||||
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, msg => _resources.TrackActor(msg.GameObjectHandler.Address));
|
||||
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, msg => _resources.UntrackActor(msg.GameObjectHandler.Address));
|
||||
}
|
||||
|
||||
private void HandlePenumbraInitialized()
|
||||
{
|
||||
Mediator.Publish(new PenumbraInitializedMessage());
|
||||
CheckModDirectory();
|
||||
_redraw.RequestImmediateRedraw(0, RedrawType.Redraw);
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
private void HandlePenumbraDisposed()
|
||||
{
|
||||
_redraw.CancelPendingRedraws();
|
||||
ModDirectory = string.Empty;
|
||||
Mediator.Publish(new PenumbraDisposedMessage());
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
private void HandlePenumbraModSettingChanged(ModSettingChange change, Guid _, string __, bool ___)
|
||||
{
|
||||
if (change == ModSettingChange.EnableState)
|
||||
{
|
||||
Mediator.Publish(new PenumbraModSettingChangedMessage());
|
||||
CheckAPI();
|
||||
ModDirectory = _penumbraResolveModDir!.Invoke().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,13 +148,196 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_redrawManager.Cancel();
|
||||
|
||||
_penumbraModSettingChanged.Dispose();
|
||||
_penumbraGameObjectResourcePathResolved.Dispose();
|
||||
_penumbraDispose.Dispose();
|
||||
_penumbraInit.Dispose();
|
||||
_penumbraObjectIsRedrawn.Dispose();
|
||||
}
|
||||
|
||||
public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collName, int idx)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var retAssign = _penumbraAssignTemporaryCollection.Invoke(collName, idx, forceAssignment: true);
|
||||
logger.LogTrace("Assigning Temp Collection {collName} to index {idx}, Success: {ret}", collName, idx, retAssign);
|
||||
return collName;
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task ConvertTextureFiles(ILogger logger, Dictionary<string, string[]> textures, IProgress<(string, int)> progress, CancellationToken token)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
_lightlessMediator.Publish(new HaltScanMessage(nameof(ConvertTextureFiles)));
|
||||
int currentTexture = 0;
|
||||
foreach (var texture in textures)
|
||||
{
|
||||
if (token.IsCancellationRequested) break;
|
||||
|
||||
progress.Report((texture.Key, ++currentTexture));
|
||||
|
||||
logger.LogInformation("Converting Texture {path} to {type}", texture.Key, TextureType.Bc7Tex);
|
||||
var convertTask = _penumbraConvertTextureFile.Invoke(texture.Key, texture.Key, TextureType.Bc7Tex, mipMaps: true);
|
||||
await convertTask.ConfigureAwait(false);
|
||||
if (convertTask.IsCompletedSuccessfully && texture.Value.Any())
|
||||
{
|
||||
foreach (var duplicatedTexture in texture.Value)
|
||||
{
|
||||
logger.LogInformation("Migrating duplicate {dup}", duplicatedTexture);
|
||||
try
|
||||
{
|
||||
File.Copy(texture.Key, duplicatedTexture, overwrite: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to copy duplicate {dup}", duplicatedTexture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_lightlessMediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFiles)));
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(async () =>
|
||||
{
|
||||
var gameObject = await _dalamudUtil.CreateGameObjectAsync(await _dalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false)).ConfigureAwait(false);
|
||||
_penumbraRedraw.Invoke(gameObject!.ObjectIndex, setting: RedrawType.Redraw);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateTemporaryCollectionAsync(ILogger logger, string uid)
|
||||
{
|
||||
if (!APIAvailable) return Guid.Empty;
|
||||
|
||||
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var collName = "Lightless_" + uid;
|
||||
_penumbraCreateNamedTemporaryCollection.Invoke(collName, collName, out var collId);
|
||||
logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId);
|
||||
return collId;
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, HashSet<string>>?> GetCharacterData(ILogger logger, GameObjectHandler handler)
|
||||
{
|
||||
if (!APIAvailable) return null;
|
||||
|
||||
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths");
|
||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
||||
if (idx == null) return null;
|
||||
return _penumbraResourcePaths.Invoke(idx.Value)[0];
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string GetMetaManipulations()
|
||||
{
|
||||
if (!APIAvailable) return string.Empty;
|
||||
return _penumbraGetMetaManipulations.Invoke();
|
||||
}
|
||||
|
||||
public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||
{
|
||||
if (!APIAvailable || _dalamudUtil.IsZoning) return;
|
||||
try
|
||||
{
|
||||
await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) =>
|
||||
{
|
||||
logger.LogDebug("[{appid}] Calling on IPC: PenumbraRedraw", applicationId);
|
||||
_penumbraRedraw!.Invoke(chara.ObjectIndex, setting: RedrawType.Redraw);
|
||||
|
||||
}, token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_redrawManager.RedrawSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collId)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("[{applicationId}] Removing temp collection for {collId}", applicationId, collId);
|
||||
var ret2 = _penumbraRemoveTemporaryCollection.Invoke(collId);
|
||||
logger.LogTrace("[{applicationId}] RemoveTemporaryCollection: {ret2}", applicationId, ret2);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
|
||||
{
|
||||
return await _penumbraResolvePaths.Invoke(forward, reverse).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collId, string manipulationData)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("[{applicationId}] Manip: {data}", applicationId, manipulationData);
|
||||
var retAdd = _penumbraAddTemporaryMod.Invoke("LightlessChara_Meta", collId, [], manipulationData, 0);
|
||||
logger.LogTrace("[{applicationId}] Setting temp meta mod for {collId}, Success: {ret}", applicationId, collId, retAdd);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collId, Dictionary<string, string> modPaths)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
foreach (var mod in modPaths)
|
||||
{
|
||||
logger.LogTrace("[{applicationId}] Change: {from} => {to}", applicationId, mod.Key, mod.Value);
|
||||
}
|
||||
var retRemove = _penumbraRemoveTemporaryMod.Invoke("LightlessChara_Files", collId, 0);
|
||||
logger.LogTrace("[{applicationId}] Removing temp files mod for {collId}, Success: {ret}", applicationId, collId, retRemove);
|
||||
var retAdd = _penumbraAddTemporaryMod.Invoke("LightlessChara_Files", collId, modPaths, string.Empty, 0);
|
||||
logger.LogTrace("[{applicationId}] Setting temp files mod for {collId}, Success: {ret}", applicationId, collId, retAdd);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void RedrawEvent(IntPtr objectAddress, int objectTableIndex)
|
||||
{
|
||||
bool wasRequested = false;
|
||||
if (_penumbraRedrawRequests.TryGetValue(objectAddress, out var redrawRequest) && redrawRequest)
|
||||
{
|
||||
_penumbraRedrawRequests[objectAddress] = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_lightlessMediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, wasRequested));
|
||||
}
|
||||
}
|
||||
|
||||
private void ResourceLoaded(IntPtr ptr, string arg1, string arg2)
|
||||
{
|
||||
if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) != 0)
|
||||
{
|
||||
_lightlessMediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2));
|
||||
}
|
||||
}
|
||||
|
||||
private void PenumbraDispose()
|
||||
{
|
||||
_redrawManager.Cancel();
|
||||
_lightlessMediator.Publish(new PenumbraDisposedMessage());
|
||||
}
|
||||
|
||||
private void PenumbraInit()
|
||||
{
|
||||
APIAvailable = true;
|
||||
ModDirectory = _penumbraResolveModDir.Invoke();
|
||||
_lightlessMediator.Publish(new PenumbraInitializedMessage());
|
||||
_penumbraRedraw!.Invoke(0, setting: RedrawType.Redraw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerPetNames : IpcServiceBase
|
||||
public sealed class IpcCallerPetNames : IIpcCaller
|
||||
{
|
||||
private static readonly IpcServiceDescriptor PetRenamerDescriptor = new("PetRenamer", "Pet Renamer", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ILogger<IpcCallerPetNames> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
@@ -27,7 +24,7 @@ public sealed class IpcCallerPetNames : IpcServiceBase
|
||||
private readonly ICallGateSubscriber<ushort, object> _clearPlayerData;
|
||||
|
||||
public IpcCallerPetNames(ILogger<IpcCallerPetNames> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, PetRenamerDescriptor)
|
||||
LightlessMediator lightlessMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
@@ -49,6 +46,25 @@ public sealed class IpcCallerPetNames : IpcServiceBase
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
try
|
||||
{
|
||||
APIAvailable = _enabled?.InvokeFunc() ?? false;
|
||||
if (APIAvailable)
|
||||
{
|
||||
APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 4, Item2: >= 0 };
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPetNicknamesReady()
|
||||
{
|
||||
CheckAPI();
|
||||
@@ -60,34 +76,6 @@ public sealed class IpcCallerPetNames : IpcServiceBase
|
||||
_lightlessMediator.Publish(new PetNamesMessage(string.Empty));
|
||||
}
|
||||
|
||||
protected override IpcConnectionState EvaluateState()
|
||||
{
|
||||
var state = base.EvaluateState();
|
||||
if (state != IpcConnectionState.Available)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var enabled = _enabled?.InvokeFunc() ?? false;
|
||||
if (!enabled)
|
||||
{
|
||||
return IpcConnectionState.PluginDisabled;
|
||||
}
|
||||
|
||||
var version = _apiVersion?.InvokeFunc() ?? (0u, 0u);
|
||||
return version.Item1 == 4 && version.Item2 >= 0
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to query Pet Renamer API version");
|
||||
return IpcConnectionState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetLocalNames()
|
||||
{
|
||||
if (!APIAvailable) return string.Empty;
|
||||
@@ -161,14 +149,8 @@ public sealed class IpcCallerPetNames : IpcServiceBase
|
||||
_lightlessMediator.Publish(new PetNamesMessage(data));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
public void Dispose()
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_petnamesReady.Unsubscribe(OnPetNicknamesReady);
|
||||
_petnamesDisposing.Unsubscribe(OnPetNicknamesDispose);
|
||||
_playerDataChanged.Unsubscribe(OnLocalPetNicknamesDataChange);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
@@ -15,7 +14,9 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
||||
private readonly ILogger<IpcProvider> _logger;
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private readonly List<IpcRegister> _ipcRegisters = [];
|
||||
private ICallGateProvider<string, IGameObject, bool>? _loadFileProvider;
|
||||
private ICallGateProvider<string, IGameObject, Task<bool>>? _loadFileAsyncProvider;
|
||||
private ICallGateProvider<List<nint>>? _handledGameAddresses;
|
||||
private readonly List<GameObjectHandler> _activeGameObjectHandlers = [];
|
||||
|
||||
public LightlessMediator Mediator { get; init; }
|
||||
@@ -43,9 +44,12 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Starting IpcProviderService");
|
||||
_ipcRegisters.Add(RegisterFunc<string, IGameObject, bool>("LightlessSync.LoadMcdf", LoadMcdf));
|
||||
_ipcRegisters.Add(RegisterFunc<string, IGameObject, Task<bool>>("LightlessSync.LoadMcdfAsync", LoadMcdfAsync));
|
||||
_ipcRegisters.Add(RegisterFunc("LightlessSync.GetHandledAddresses", GetHandledAddresses));
|
||||
_loadFileProvider = _pi.GetIpcProvider<string, IGameObject, bool>("LightlessSync.LoadMcdf");
|
||||
_loadFileProvider.RegisterFunc(LoadMcdf);
|
||||
_loadFileAsyncProvider = _pi.GetIpcProvider<string, IGameObject, Task<bool>>("LightlessSync.LoadMcdfAsync");
|
||||
_loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync);
|
||||
_handledGameAddresses = _pi.GetIpcProvider<List<nint>>("LightlessSync.GetHandledAddresses");
|
||||
_handledGameAddresses.RegisterFunc(GetHandledAddresses);
|
||||
_logger.LogInformation("Started IpcProviderService");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -53,11 +57,9 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("Stopping IpcProvider Service");
|
||||
foreach (var register in _ipcRegisters)
|
||||
{
|
||||
register.Dispose();
|
||||
}
|
||||
_ipcRegisters.Clear();
|
||||
_loadFileProvider?.UnregisterFunc();
|
||||
_loadFileAsyncProvider?.UnregisterFunc();
|
||||
_handledGameAddresses?.UnregisterFunc();
|
||||
Mediator.UnsubscribeAll(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -87,40 +89,4 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList();
|
||||
}
|
||||
|
||||
private IpcRegister RegisterFunc(string label, Func<List<nint>> handler)
|
||||
{
|
||||
var provider = _pi.GetIpcProvider<List<nint>>(label);
|
||||
provider.RegisterFunc(handler);
|
||||
return new IpcRegister(provider.UnregisterFunc);
|
||||
}
|
||||
|
||||
private IpcRegister RegisterFunc<T1, T2, TRet>(string label, Func<T1, T2, TRet> handler)
|
||||
{
|
||||
var provider = _pi.GetIpcProvider<T1, T2, TRet>(label);
|
||||
provider.RegisterFunc(handler);
|
||||
return new IpcRegister(provider.UnregisterFunc);
|
||||
}
|
||||
|
||||
private sealed class IpcRegister : IDisposable
|
||||
{
|
||||
private readonly Action _unregister;
|
||||
private bool _disposed;
|
||||
|
||||
public IpcRegister(Action unregister)
|
||||
{
|
||||
_unregister = unregister;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_unregister();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||
|
||||
public abstract class PenumbraBase : IpcInteropBase
|
||||
{
|
||||
protected PenumbraBase(
|
||||
ILogger logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator) : base(logger)
|
||||
{
|
||||
PluginInterface = pluginInterface;
|
||||
DalamudUtil = dalamudUtil;
|
||||
Mediator = mediator;
|
||||
}
|
||||
|
||||
protected IDalamudPluginInterface PluginInterface { get; }
|
||||
|
||||
protected DalamudUtilService DalamudUtil { get; }
|
||||
|
||||
protected LightlessMediator Mediator { get; }
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||
|
||||
public sealed class PenumbraCollections : PenumbraBase
|
||||
{
|
||||
private readonly CreateTemporaryCollection _createNamedTemporaryCollection;
|
||||
private readonly AssignTemporaryCollection _assignTemporaryCollection;
|
||||
private readonly DeleteTemporaryCollection _removeTemporaryCollection;
|
||||
private readonly AddTemporaryMod _addTemporaryMod;
|
||||
private readonly RemoveTemporaryMod _removeTemporaryMod;
|
||||
private readonly GetCollections _getCollections;
|
||||
private readonly ConcurrentDictionary<Guid, string> _activeTemporaryCollections = new();
|
||||
|
||||
private int _cleanupScheduled;
|
||||
|
||||
public PenumbraCollections(
|
||||
ILogger logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator) : base(logger, pluginInterface, dalamudUtil, mediator)
|
||||
{
|
||||
_createNamedTemporaryCollection = new CreateTemporaryCollection(pluginInterface);
|
||||
_assignTemporaryCollection = new AssignTemporaryCollection(pluginInterface);
|
||||
_removeTemporaryCollection = new DeleteTemporaryCollection(pluginInterface);
|
||||
_addTemporaryMod = new AddTemporaryMod(pluginInterface);
|
||||
_removeTemporaryMod = new RemoveTemporaryMod(pluginInterface);
|
||||
_getCollections = new GetCollections(pluginInterface);
|
||||
}
|
||||
|
||||
public override string Name => "Penumbra.Collections";
|
||||
|
||||
public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collectionId, int objectIndex)
|
||||
{
|
||||
if (!IsAvailable || collectionId == Guid.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var result = _assignTemporaryCollection.Invoke(collectionId, objectIndex, forceAssignment: true);
|
||||
logger.LogTrace("Assigning Temp Collection {CollectionId} to index {ObjectIndex}, Success: {Result}", collectionId, objectIndex, result);
|
||||
return result;
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateTemporaryCollectionAsync(ILogger logger, string uid)
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
var (collectionId, collectionName) = await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var name = $"Lightless_{uid}";
|
||||
_createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId);
|
||||
logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}", name, tempCollectionId);
|
||||
return (tempCollectionId, name);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (collectionId != Guid.Empty)
|
||||
{
|
||||
_activeTemporaryCollections[collectionId] = collectionName;
|
||||
}
|
||||
|
||||
return collectionId;
|
||||
}
|
||||
|
||||
public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collectionId)
|
||||
{
|
||||
if (!IsAvailable || collectionId == Guid.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("[{ApplicationId}] Removing temp collection for {CollectionId}", applicationId, collectionId);
|
||||
var result = _removeTemporaryCollection.Invoke(collectionId);
|
||||
logger.LogTrace("[{ApplicationId}] RemoveTemporaryCollection: {Result}", applicationId, result);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
||||
}
|
||||
|
||||
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, IReadOnlyDictionary<string, string> modPaths)
|
||||
{
|
||||
if (!IsAvailable || collectionId == Guid.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
foreach (var mod in modPaths)
|
||||
{
|
||||
logger.LogTrace("[{ApplicationId}] Change: {From} => {To}", applicationId, mod.Key, mod.Value);
|
||||
}
|
||||
|
||||
var removeResult = _removeTemporaryMod.Invoke("LightlessChara_Files", collectionId, 0);
|
||||
logger.LogTrace("[{ApplicationId}] Removing temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, removeResult);
|
||||
|
||||
var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, new Dictionary<string, string>(modPaths), string.Empty, 0);
|
||||
logger.LogTrace("[{ApplicationId}] Setting temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, addResult);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData)
|
||||
{
|
||||
if (!IsAvailable || collectionId == Guid.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("[{ApplicationId}] Manip: {Data}", applicationId, manipulationData);
|
||||
var result = _addTemporaryMod.Invoke("LightlessChara_Meta", collectionId, [], manipulationData, 0);
|
||||
logger.LogTrace("[{ApplicationId}] Setting temp meta mod for {CollectionId}, Success: {Result}", applicationId, collectionId, result);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
if (current == IpcConnectionState.Available)
|
||||
{
|
||||
ScheduleCleanup();
|
||||
}
|
||||
else if (previous == IpcConnectionState.Available && current != IpcConnectionState.Available)
|
||||
{
|
||||
Interlocked.Exchange(ref _cleanupScheduled, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleCleanup()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _cleanupScheduled, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(CleanupTemporaryCollectionsAsync);
|
||||
}
|
||||
|
||||
private async Task CleanupTemporaryCollectionsAsync()
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var collections = await DalamudUtil.RunOnFrameworkThread(() => _getCollections.Invoke()).ConfigureAwait(false);
|
||||
foreach (var (collectionId, name) in collections)
|
||||
{
|
||||
if (!IsLightlessCollectionName(name) || _activeTemporaryCollections.ContainsKey(collectionId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId);
|
||||
var deleteResult = await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var result = (PenumbraApiEc)_removeTemporaryCollection.Invoke(collectionId);
|
||||
Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result);
|
||||
return result;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (deleteResult == PenumbraApiEc.Success)
|
||||
{
|
||||
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("Skipped removing temporary collection {CollectionName} ({CollectionId}). Result: {Result}", name, collectionId, deleteResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to clean up Penumbra temporary collections");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsLightlessCollectionName(string? name)
|
||||
=> !string.IsNullOrEmpty(name) && name.StartsWith("Lightless_", StringComparison.Ordinal);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||
|
||||
public sealed class PenumbraRedraw : PenumbraBase
|
||||
{
|
||||
private readonly RedrawManager _redrawManager;
|
||||
private readonly RedrawObject _penumbraRedraw;
|
||||
private readonly EventSubscriber<nint, int> _penumbraObjectIsRedrawn;
|
||||
|
||||
public PenumbraRedraw(
|
||||
ILogger logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator,
|
||||
RedrawManager redrawManager) : base(logger, pluginInterface, dalamudUtil, mediator)
|
||||
{
|
||||
_redrawManager = redrawManager;
|
||||
|
||||
_penumbraRedraw = new RedrawObject(pluginInterface);
|
||||
_penumbraObjectIsRedrawn = GameObjectRedrawn.Subscriber(pluginInterface, HandlePenumbraRedrawEvent);
|
||||
}
|
||||
|
||||
public override string Name => "Penumbra.Redraw";
|
||||
|
||||
public void CancelPendingRedraws()
|
||||
=> _redrawManager.Cancel();
|
||||
|
||||
public void RequestImmediateRedraw(int objectIndex, RedrawType redrawType)
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_penumbraRedraw.Invoke(objectIndex, redrawType);
|
||||
}
|
||||
|
||||
public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||
{
|
||||
if (!IsAvailable || DalamudUtil.IsZoning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var redrawSemaphore = _redrawManager.RedrawSemaphore;
|
||||
var semaphoreAcquired = false;
|
||||
|
||||
try
|
||||
{
|
||||
await redrawSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
semaphoreAcquired = true;
|
||||
|
||||
await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, chara =>
|
||||
{
|
||||
logger.LogDebug("[{ApplicationId}] Calling on IPC: PenumbraRedraw", applicationId);
|
||||
_penumbraRedraw.Invoke(chara.ObjectIndex, RedrawType.Redraw);
|
||||
}, token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (semaphoreAcquired)
|
||||
{
|
||||
redrawSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePenumbraRedrawEvent(IntPtr objectAddress, int objectTableIndex)
|
||||
=> Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, false));
|
||||
|
||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
_penumbraObjectIsRedrawn.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||
|
||||
public sealed class PenumbraResource : PenumbraBase
|
||||
{
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private readonly GetGameObjectResourcePaths _gameObjectResourcePaths;
|
||||
private readonly ResolvePlayerPathsAsync _resolvePlayerPaths;
|
||||
private readonly GetPlayerMetaManipulations _getPlayerMetaManipulations;
|
||||
private readonly EventSubscriber<nint, string, string> _gameObjectResourcePathResolved;
|
||||
private readonly ConcurrentDictionary<IntPtr, byte> _trackedActors = new();
|
||||
|
||||
public PenumbraResource(
|
||||
ILogger logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator,
|
||||
ActorObjectService actorObjectService) : base(logger, pluginInterface, dalamudUtil, mediator)
|
||||
{
|
||||
_actorObjectService = actorObjectService;
|
||||
_gameObjectResourcePaths = new GetGameObjectResourcePaths(pluginInterface);
|
||||
_resolvePlayerPaths = new ResolvePlayerPathsAsync(pluginInterface);
|
||||
_getPlayerMetaManipulations = new GetPlayerMetaManipulations(pluginInterface);
|
||||
_gameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pluginInterface, HandleResourceLoaded);
|
||||
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
{
|
||||
TrackActor(descriptor.Address);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Name => "Penumbra.Resources";
|
||||
|
||||
public async Task<Dictionary<string, HashSet<string>>?> GetCharacterDataAsync(ILogger logger, GameObjectHandler handler)
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await DalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths");
|
||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
||||
if (idx == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _gameObjectResourcePaths.Invoke(idx.Value)[0];
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string GetMetaManipulations()
|
||||
=> IsAvailable ? _getPlayerMetaManipulations.Invoke() : string.Empty;
|
||||
|
||||
public async Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forwardPaths, string[] reversePaths)
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return (Array.Empty<string>(), Array.Empty<string[]>());
|
||||
}
|
||||
|
||||
return await _resolvePlayerPaths.Invoke(forwardPaths, reversePaths).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void TrackActor(nint address)
|
||||
{
|
||||
if (address != nint.Zero)
|
||||
{
|
||||
_trackedActors[(IntPtr)address] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void UntrackActor(nint address)
|
||||
{
|
||||
if (address != nint.Zero)
|
||||
{
|
||||
_trackedActors.TryRemove((IntPtr)address, out _);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleResourceLoaded(nint ptr, string resolvedPath, string gamePath)
|
||||
{
|
||||
if (ptr == nint.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_trackedActors.ContainsKey(ptr))
|
||||
{
|
||||
var descriptor = _actorObjectService.PlayerDescriptors.FirstOrDefault(d => d.Address == ptr);
|
||||
if (descriptor.Address != nint.Zero)
|
||||
{
|
||||
_trackedActors[ptr] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Compare(resolvedPath, gamePath, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, resolvedPath, gamePath));
|
||||
}
|
||||
|
||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
if (current != IpcConnectionState.Available)
|
||||
{
|
||||
_trackedActors.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
{
|
||||
TrackActor(descriptor.Address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
_gameObjectResourcePathResolved.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||
|
||||
public sealed class PenumbraTexture : PenumbraBase
|
||||
{
|
||||
private readonly PenumbraRedraw _redrawFeature;
|
||||
private readonly ConvertTextureFile _convertTextureFile;
|
||||
|
||||
public PenumbraTexture(
|
||||
ILogger logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessMediator mediator,
|
||||
PenumbraRedraw redrawFeature) : base(logger, pluginInterface, dalamudUtil, mediator)
|
||||
{
|
||||
_redrawFeature = redrawFeature;
|
||||
_convertTextureFile = new ConvertTextureFile(pluginInterface);
|
||||
}
|
||||
|
||||
public override string Name => "Penumbra.Textures";
|
||||
|
||||
public async Task ConvertTextureFilesAsync(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
||||
{
|
||||
if (!IsAvailable || jobs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Publish(new HaltScanMessage(nameof(ConvertTextureFilesAsync)));
|
||||
|
||||
var totalJobs = jobs.Count;
|
||||
var completedJobs = 0;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
progress?.Report(new TextureConversionProgress(completedJobs, totalJobs, job));
|
||||
await ConvertSingleJobAsync(logger, job, token).ConfigureAwait(false);
|
||||
completedJobs++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFilesAsync)));
|
||||
}
|
||||
|
||||
if (completedJobs > 0 && !token.IsCancellationRequested)
|
||||
{
|
||||
await DalamudUtil.RunOnFrameworkThread(async () =>
|
||||
{
|
||||
var player = await DalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false);
|
||||
if (player == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var gameObject = await DalamudUtil.CreateGameObjectAsync(player).ConfigureAwait(false);
|
||||
if (gameObject == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_redrawFeature.RequestImmediateRedraw(gameObject.ObjectIndex, RedrawType.Redraw);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token)
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ConvertSingleJobAsync(Logger, job, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ConvertSingleJobAsync(ILogger logger, TextureConversionJob job, CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
|
||||
var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps);
|
||||
await convertTask.ConfigureAwait(false);
|
||||
|
||||
if (!convertTask.IsCompletedSuccessfully || job.DuplicateTargets is not { Count: > 0 })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var duplicate in job.DuplicateTargets)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogInformation("Synchronizing duplicate {Duplicate}", duplicate);
|
||||
File.Copy(job.OutputFile, duplicate, overwrite: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to copy duplicate {Duplicate}", duplicate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using Penumbra.Api.Enums;
|
||||
|
||||
namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single texture conversion request, including optional duplicate targets.
|
||||
/// </summary>
|
||||
public sealed record TextureConversionJob(
|
||||
string InputFile,
|
||||
string OutputFile,
|
||||
TextureType TargetType,
|
||||
bool IncludeMipMaps = true,
|
||||
IReadOnlyList<string>? DuplicateTargets = null);
|
||||
|
||||
/// <summary>
|
||||
/// Progress payload for a texture conversion batch.
|
||||
/// </summary>
|
||||
/// <param name="Completed">Number of completed conversions.</param>
|
||||
/// <param name="Total">Total number of conversions scheduled.</param>
|
||||
/// <param name="CurrentJob">The job currently being processed.</param>
|
||||
public sealed record TextureConversionProgress(int Completed, int Total, TextureConversionJob CurrentJob);
|
||||
@@ -1,14 +0,0 @@
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration;
|
||||
|
||||
public sealed class ChatConfigService : ConfigurationServiceBase<ChatConfig>
|
||||
{
|
||||
public const string ConfigName = "chatconfig.json";
|
||||
|
||||
public ChatConfigService(string configDir) : base(configDir)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ConfigurationName => ConfigName;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
[Serializable]
|
||||
public sealed class ChatConfig : ILightlessConfiguration
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public bool AutoEnableChatOnLogin { get; set; } = false;
|
||||
public bool ShowRulesOverlayOnOpen { get; set; } = true;
|
||||
public bool ShowMessageTimestamps { get; set; } = true;
|
||||
public float ChatWindowOpacity { get; set; } = .97f;
|
||||
public bool IsWindowPinned { get; set; } = false;
|
||||
public bool AutoOpenChatOnPluginLoad { get; set; } = false;
|
||||
public float ChatFontScale { get; set; } = 1.0f;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
public enum LightfinderDtrDisplayMode
|
||||
{
|
||||
NearbyBroadcasts = 0,
|
||||
PendingPairRequests = 1,
|
||||
}
|
||||
@@ -2,7 +2,6 @@ using Dalamud.Game.Text;
|
||||
using LightlessSync.UtilsEnum.Enum;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
@@ -23,13 +22,6 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
||||
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
||||
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
||||
public bool ShowLightfinderInDtr { get; set; } = false;
|
||||
public bool UseLightfinderColorsInDtr { get; set; } = true;
|
||||
public DtrEntry.Colors DtrColorsLightfinderEnabled { get; set; } = new(Foreground: 0xB590FFu, Glow: 0x4F406Eu);
|
||||
public DtrEntry.Colors DtrColorsLightfinderDisabled { get; set; } = new(Foreground: 0xD44444u, Glow: 0x642222u);
|
||||
public DtrEntry.Colors DtrColorsLightfinderCooldown { get; set; } = new(Foreground: 0xFFE97Au, Glow: 0x766C3Au);
|
||||
public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u);
|
||||
public LightfinderDtrDisplayMode LightfinderDtrDisplayMode { get; set; } = LightfinderDtrDisplayMode.PendingPairRequests;
|
||||
public bool UseLightlessRedesign { get; set; } = true;
|
||||
public bool EnableRightClickMenus { get; set; } = true;
|
||||
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
||||
@@ -49,7 +41,6 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Default;
|
||||
public float ProfileDelay { get; set; } = 1.5f;
|
||||
public bool ProfilePopoutRight { get; set; } = false;
|
||||
public bool ProfilesAllowNsfw { get; set; } = false;
|
||||
@@ -63,16 +54,11 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public bool ShowOnlineNotifications { get; set; } = false;
|
||||
public bool ShowOnlineNotificationsOnlyForIndividualPairs { get; set; } = true;
|
||||
public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false;
|
||||
public bool ShowVisiblePairsGreenEye { get; set; } = false;
|
||||
public bool ShowTransferBars { get; set; } = true;
|
||||
public bool ShowTransferWindow { get; set; } = false;
|
||||
public bool ShowPlayerLinesTransferWindow { get; set; } = true;
|
||||
public bool ShowPlayerSpeedBarsTransferWindow { get; set; } = true;
|
||||
public bool UseNotificationsForDownloads { get; set; } = true;
|
||||
public bool ShowUploading { get; set; } = true;
|
||||
public bool ShowUploadingBigText { get; set; } = true;
|
||||
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
||||
public bool EnableDirectDownloads { get; set; } = true;
|
||||
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
||||
public int TransferBarsHeight { get; set; } = 12;
|
||||
public bool TransferBarsShowText { get; set; } = true;
|
||||
@@ -83,68 +69,17 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false;
|
||||
public int Version { get; set; } = 1;
|
||||
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
|
||||
|
||||
// Lightless Notification Configuration
|
||||
public bool UseLightlessNotifications { get; set; } = true;
|
||||
public bool ShowNotificationProgress { get; set; } = true;
|
||||
public NotificationLocation LightlessInfoNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||
public NotificationLocation LightlessWarningNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||
public NotificationLocation LightlessErrorNotification { get; set; } = NotificationLocation.ChatAndLightlessUi;
|
||||
public NotificationLocation LightlessPairRequestNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||
public NotificationLocation LightlessDownloadNotification { get; set; } = NotificationLocation.TextOverlay;
|
||||
public NotificationLocation LightlessPerformanceNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||
|
||||
// Basic Settings
|
||||
public float NotificationOpacity { get; set; } = 0.95f;
|
||||
public int MaxSimultaneousNotifications { get; set; } = 5;
|
||||
public bool AutoDismissOnAction { get; set; } = true;
|
||||
public bool DismissNotificationOnClick { get; set; } = false;
|
||||
public bool ShowNotificationTimestamp { get; set; } = false;
|
||||
|
||||
// Position & Layout
|
||||
public NotificationCorner NotificationCorner { get; set; } = NotificationCorner.Right;
|
||||
public int NotificationOffsetY { get; set; } = 50;
|
||||
public int NotificationOffsetX { get; set; } = 0;
|
||||
public float NotificationWidth { get; set; } = 350f;
|
||||
public float NotificationSpacing { get; set; } = 8f;
|
||||
|
||||
// Animation & Effects
|
||||
public float NotificationAnimationSpeed { get; set; } = 10f;
|
||||
public float NotificationSlideSpeed { get; set; } = 10f;
|
||||
public float NotificationAccentBarWidth { get; set; } = 3f;
|
||||
|
||||
// Duration per Type
|
||||
public int InfoNotificationDurationSeconds { get; set; } = 10;
|
||||
public int WarningNotificationDurationSeconds { get; set; } = 15;
|
||||
public int ErrorNotificationDurationSeconds { get; set; } = 20;
|
||||
public int PairRequestDurationSeconds { get; set; } = 180;
|
||||
public int DownloadNotificationDurationSeconds { get; set; } = 30;
|
||||
public int PerformanceNotificationDurationSeconds { get; set; } = 20;
|
||||
public uint CustomInfoSoundId { get; set; } = 2; // Se2
|
||||
public uint CustomWarningSoundId { get; set; } = 16; // Se15
|
||||
public uint CustomErrorSoundId { get; set; } = 16; // Se15
|
||||
public uint PairRequestSoundId { get; set; } = 5; // Se5
|
||||
public uint PerformanceSoundId { get; set; } = 16; // Se15
|
||||
public bool DisableInfoSound { get; set; } = true;
|
||||
public bool DisableWarningSound { get; set; } = true;
|
||||
public bool DisableErrorSound { get; set; } = true;
|
||||
public bool DisablePairRequestSound { get; set; } = true;
|
||||
public bool DisablePerformanceSound { get; set; } = true;
|
||||
public bool ShowPerformanceNotificationActions { get; set; } = true;
|
||||
public bool ShowPairRequestNotificationActions { get; set; } = true;
|
||||
public bool UseFocusTarget { get; set; } = false;
|
||||
public bool overrideFriendColor { get; set; } = false;
|
||||
public bool overridePartyColor { get; set; } = false;
|
||||
public bool overrideFcTagColor { get; set; } = false;
|
||||
public bool useColoredUIDs { get; set; } = true;
|
||||
public bool BroadcastEnabled { get; set; } = false;
|
||||
public bool LightfinderAutoEnableOnConnect { get; set; } = false;
|
||||
public short LightfinderLabelOffsetX { get; set; } = 0;
|
||||
public short LightfinderLabelOffsetY { get; set; } = 0;
|
||||
public bool LightfinderLabelUseIcon { get; set; } = false;
|
||||
public bool LightfinderLabelShowOwn { get; set; } = true;
|
||||
public bool LightfinderLabelShowPaired { get; set; } = true;
|
||||
public bool LightfinderLabelShowHidden { get; set; } = false;
|
||||
public string LightfinderLabelIconGlyph { get; set; } = SeIconCharExtensions.ToIconString(SeIconChar.Hyadelyn);
|
||||
public float LightfinderLabelScale { get; set; } = 1.0f;
|
||||
public bool LightfinderAutoAlign { get; set; } = true;
|
||||
@@ -152,5 +87,4 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public DateTime BroadcastTtl { get; set; } = DateTime.MinValue;
|
||||
public bool SyncshellFinderEnabled { get; set; } = false;
|
||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||
public string LastSeenVersion { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
|
||||
{
|
||||
public int Version { get; set; } = 1;
|
||||
public bool ShowPerformanceIndicator { get; set; } = true;
|
||||
public bool ShowPerformanceUsageNextToName { get; set; } = false;
|
||||
public bool WarnOnExceedingThresholds { get; set; } = true;
|
||||
public bool WarnOnPreferredPermissionsExceedingThresholds { get; set; } = false;
|
||||
public int VRAMSizeWarningThresholdMiB { get; set; } = 375;
|
||||
@@ -17,9 +16,4 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
|
||||
public bool PauseInInstanceDuty { get; set; } = false;
|
||||
public bool PauseWhilePerforming { get; set; } = true;
|
||||
public bool PauseInCombat { get; set; } = true;
|
||||
public bool EnableNonIndexTextureMipTrim { get; set; } = false;
|
||||
public bool EnableIndexTextureDownscale { get; set; } = false;
|
||||
public int TextureDownscaleMaxDimension { get; set; } = 2048;
|
||||
public bool OnlyDownscaleUncompressedTextures { get; set; } = true;
|
||||
public bool KeepOriginalTextureFiles { get; set; } = false;
|
||||
}
|
||||
@@ -13,8 +13,6 @@ public class TransientConfig : ILightlessConfiguration
|
||||
public Dictionary<uint, List<string>> JobSpecificCache { get; set; } = [];
|
||||
public Dictionary<uint, List<string>> JobSpecificPetCache { get; set; } = [];
|
||||
|
||||
private readonly object _cacheLock = new();
|
||||
|
||||
public TransientPlayerConfig()
|
||||
{
|
||||
|
||||
@@ -41,51 +39,45 @@ public class TransientConfig : ILightlessConfiguration
|
||||
|
||||
public int RemovePath(string gamePath, ObjectKind objectKind)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
int removedEntries = 0;
|
||||
if (objectKind == ObjectKind.Player)
|
||||
{
|
||||
int removedEntries = 0;
|
||||
if (objectKind == ObjectKind.Player)
|
||||
if (GlobalPersistentCache.Remove(gamePath)) removedEntries++;
|
||||
foreach (var kvp in JobSpecificCache)
|
||||
{
|
||||
if (GlobalPersistentCache.Remove(gamePath)) removedEntries++;
|
||||
foreach (var kvp in JobSpecificCache)
|
||||
{
|
||||
if (kvp.Value.Remove(gamePath)) removedEntries++;
|
||||
}
|
||||
if (kvp.Value.Remove(gamePath)) removedEntries++;
|
||||
}
|
||||
if (objectKind == ObjectKind.Pet)
|
||||
{
|
||||
foreach (var kvp in JobSpecificPetCache)
|
||||
{
|
||||
if (kvp.Value.Remove(gamePath)) removedEntries++;
|
||||
}
|
||||
}
|
||||
return removedEntries;
|
||||
}
|
||||
if (objectKind == ObjectKind.Pet)
|
||||
{
|
||||
foreach (var kvp in JobSpecificPetCache)
|
||||
{
|
||||
if (kvp.Value.Remove(gamePath)) removedEntries++;
|
||||
}
|
||||
}
|
||||
return removedEntries;
|
||||
}
|
||||
|
||||
public void AddOrElevate(uint jobId, string gamePath)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
// check if it's in the global cache, if yes, do nothing
|
||||
if (GlobalPersistentCache.Contains(gamePath, StringComparer.Ordinal))
|
||||
{
|
||||
// check if it's in the global cache, if yes, do nothing
|
||||
if (GlobalPersistentCache.Contains(gamePath, StringComparer.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ElevateIfNeeded(jobId, gamePath)) return;
|
||||
if (ElevateIfNeeded(jobId, gamePath)) return;
|
||||
|
||||
// check if the jobid is already in the cache to start
|
||||
if (!JobSpecificCache.TryGetValue(jobId, out var jobCache))
|
||||
{
|
||||
JobSpecificCache[jobId] = jobCache = new();
|
||||
}
|
||||
// check if the jobid is already in the cache to start
|
||||
if (!JobSpecificCache.TryGetValue(jobId, out var jobCache))
|
||||
{
|
||||
JobSpecificCache[jobId] = jobCache = new();
|
||||
}
|
||||
|
||||
// check if the path is already in the job specific cache
|
||||
if (!jobCache.Contains(gamePath, StringComparer.Ordinal))
|
||||
{
|
||||
jobCache.Add(gamePath);
|
||||
}
|
||||
// check if the path is already in the job specific cache
|
||||
if (!jobCache.Contains(gamePath, StringComparer.Ordinal))
|
||||
{
|
||||
jobCache.Add(gamePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
[Serializable]
|
||||
public class UiStyleOverride
|
||||
{
|
||||
public uint? Color { get; set; }
|
||||
public float? Float { get; set; }
|
||||
public Vector2Config? Vector2 { get; set; }
|
||||
|
||||
public bool IsEmpty => Color is null && Float is null && Vector2 is null;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public record struct Vector2Config(float X, float Y)
|
||||
{
|
||||
public static implicit operator Vector2(Vector2Config value) => new(value.X, value.Y);
|
||||
public static implicit operator Vector2Config(Vector2 value) => new(value.X, value.Y);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
[Serializable]
|
||||
public class UiThemeConfig : ILightlessConfiguration
|
||||
{
|
||||
public Dictionary<string, UiStyleOverride> StyleOverrides { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public int Version { get; set; } = 1;
|
||||
}
|
||||
@@ -1,28 +1,16 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Models;
|
||||
namespace LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
public enum NotificationLocation
|
||||
{
|
||||
Nowhere,
|
||||
Chat,
|
||||
Toast,
|
||||
Both,
|
||||
LightlessUi,
|
||||
ChatAndLightlessUi,
|
||||
TextOverlay,
|
||||
Both
|
||||
}
|
||||
|
||||
public enum NotificationType
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
PairRequest,
|
||||
Download,
|
||||
Performance
|
||||
}
|
||||
|
||||
public enum NotificationCorner
|
||||
{
|
||||
Right,
|
||||
Left
|
||||
Error
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Models.Obsolete;
|
||||
|
||||
[Serializable]
|
||||
[Obsolete("Deprecated, use ServerStorage")]
|
||||
public class ServerStorageV0
|
||||
{
|
||||
public List<Authentication> Authentications { get; set; } = [];
|
||||
public bool FullPause { get; set; } = false;
|
||||
public Dictionary<string, string> GidServerComments { get; set; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> OpenPairTags { get; set; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<int, SecretKey> SecretKeys { get; set; } = [];
|
||||
public HashSet<string> ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal);
|
||||
public string ServerName { get; set; } = string.Empty;
|
||||
public string ServerUri { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> UidServerComments { get; set; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<string, List<string>> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal);
|
||||
|
||||
public ServerStorage ToV1()
|
||||
{
|
||||
return new ServerStorage()
|
||||
{
|
||||
ServerUri = ServerUri,
|
||||
ServerName = ServerName,
|
||||
Authentications = [.. Authentications],
|
||||
FullPause = FullPause,
|
||||
SecretKeys = SecretKeys.ToDictionary(p => p.Key, p => p.Value)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,5 @@ public class ServerStorage
|
||||
public bool UseOAuth2 { get; set; } = false;
|
||||
public string? OAuthToken { get; set; } = null;
|
||||
public HttpTransportType HttpTransportType { get; set; } = HttpTransportType.WebSockets;
|
||||
public bool ForceWebSockets { get; set; } = false;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration;
|
||||
|
||||
public class UiThemeConfigService : ConfigurationServiceBase<UiThemeConfig>
|
||||
{
|
||||
public const string ConfigName = "ui-theme.json";
|
||||
|
||||
public UiThemeConfigService(string configDir) : base(configDir)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ConfigurationName => ConfigName;
|
||||
}
|
||||
@@ -9,7 +9,6 @@ using LightlessSync.UI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using System.Reflection;
|
||||
|
||||
namespace LightlessSync;
|
||||
@@ -102,7 +101,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
||||
|
||||
UIColors.Initialize(_lightlessConfigService);
|
||||
Mediator.StartQueueProcessing();
|
||||
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -116,24 +115,6 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void CheckVersion()
|
||||
{
|
||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
var currentVersion = ver == null ? string.Empty : $"{ver.Major}.{ver.Minor}.{ver.Build}";
|
||||
var lastSeen = _lightlessConfigService.Current.LastSeenVersion ?? string.Empty;
|
||||
Logger.LogInformation("Last seen version: {lastSeen}, current version: {currentVersion}", lastSeen, currentVersion);
|
||||
Logger.LogInformation("User has valid setup: {hasValidSetup}", _lightlessConfigService.Current.HasValidSetup());
|
||||
Logger.LogInformation("Server has valid config: {hasValidConfig}", _serverConfigurationManager.HasValidConfig());
|
||||
// Show update notes if version has changed and user has valid setup
|
||||
|
||||
if (!string.Equals(lastSeen, currentVersion, StringComparison.Ordinal) &&
|
||||
_lightlessConfigService.Current.HasValidSetup() &&
|
||||
_serverConfigurationManager.HasValidConfig())
|
||||
{
|
||||
Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi)));
|
||||
}
|
||||
}
|
||||
|
||||
private void DalamudUtilOnLogIn()
|
||||
{
|
||||
@@ -173,7 +154,6 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<VisibleUserDataDistributor>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NameplateService>();
|
||||
CheckVersion();
|
||||
|
||||
#if !DEBUG
|
||||
if (_lightlessConfigService.Current.LogLevel != LogLevel.Information)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Dalamud.NET.Sdk/14.0.1">
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
||||
<PropertyGroup>
|
||||
<Authors></Authors>
|
||||
<Company></Company>
|
||||
<Version>1.42.0.69</Version>
|
||||
<Version>1.12.1.1</Version>
|
||||
<Description></Description>
|
||||
<Copyright></Copyright>
|
||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0-windows7.0</TargetFramework>
|
||||
<TargetFramework>net9.0-windows7.0</TargetFramework>
|
||||
<Platforms>x64</Platforms>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
@@ -27,26 +27,25 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blake3" Version="2.0.0" />
|
||||
<PackageReference Include="Brio.API" Version="3.0.0" />
|
||||
<PackageReference Include="DalamudPackager" Version="13.0.0" />
|
||||
<PackageReference Include="Downloader" Version="4.0.3" />
|
||||
<PackageReference Include="K4os.Compression.LZ4.Legacy" Version="1.3.8" />
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="2.0.212">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||
<PackageReference Include="Glamourer.Api" Version="2.8.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
|
||||
<PackageReference Include="Glamourer.Api" Version="2.6.0" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.2.2" />
|
||||
<PackageReference Include="Penumbra.String" Version="1.0.5" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.7.0.110445">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -65,8 +64,6 @@
|
||||
<None Update="images\icon.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<EmbeddedResource Include="Changelog\changelog.yaml" />
|
||||
<EmbeddedResource Include="Changelog\credits.yaml" />
|
||||
<EmbeddedResource Include="Localization\de.json" />
|
||||
<EmbeddedResource Include="Localization\fr.json" />
|
||||
</ItemGroup>
|
||||
@@ -77,28 +74,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
|
||||
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
|
||||
<ProjectReference Include="..\ffxiv_pictomancy\Pictomancy\Pictomancy.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="OtterTex">
|
||||
<HintPath>lib\OtterTex.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="lib\DirectXTexC.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>DirectXTexC.dll</TargetPath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="DalamudPackager" Version="14.0.1" />
|
||||
<ProjectReference Include="..\PenumbraAPI\Penumbra.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using LightlessSync.API.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using LightlessSync.API.Data;
|
||||
|
||||
namespace LightlessSync.PlayerData.Data;
|
||||
|
||||
@@ -16,42 +13,37 @@ public class FileReplacementDataComparer : IEqualityComparer<FileReplacementData
|
||||
|
||||
public bool Equals(FileReplacementData? x, FileReplacementData? y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
return true;
|
||||
if (x is null || y is null)
|
||||
return false;
|
||||
|
||||
return string.Equals(x.Hash, y.Hash, StringComparison.OrdinalIgnoreCase)
|
||||
&& ComparePathSets(x.GamePaths, y.GamePaths)
|
||||
&& string.Equals(x.FileSwapPath ?? string.Empty, y.FileSwapPath ?? string.Empty, StringComparison.Ordinal);
|
||||
if (x == null || y == null) return false;
|
||||
return x.Hash.Equals(y.Hash) && CompareHashSets(x.GamePaths.ToHashSet(StringComparer.Ordinal), y.GamePaths.ToHashSet(StringComparer.Ordinal)) && string.Equals(x.FileSwapPath, y.FileSwapPath, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public int GetHashCode(FileReplacementData obj)
|
||||
{
|
||||
if (obj is null)
|
||||
return 0;
|
||||
|
||||
var hash = StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Hash ?? string.Empty);
|
||||
hash = HashCode.Combine(hash, GetSetHashCode(obj.GamePaths));
|
||||
hash = HashCode.Combine(hash, StringComparer.OrdinalIgnoreCase.GetHashCode(obj.FileSwapPath ?? string.Empty));
|
||||
return hash;
|
||||
return HashCode.Combine(obj.Hash.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths), StringComparer.Ordinal.GetHashCode(obj.FileSwapPath));
|
||||
}
|
||||
|
||||
private static bool ComparePathSets(IEnumerable<string> first, IEnumerable<string> second)
|
||||
private static bool CompareHashSets(HashSet<string> list1, HashSet<string> list2)
|
||||
{
|
||||
var left = new HashSet<string>(first ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
var right = new HashSet<string>(second ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase);
|
||||
return left.SetEquals(right);
|
||||
}
|
||||
if (list1.Count != list2.Count)
|
||||
return false;
|
||||
|
||||
private static int GetSetHashCode(IEnumerable<string> paths)
|
||||
{
|
||||
int hash = 0;
|
||||
foreach (var element in paths ?? Enumerable.Empty<string>())
|
||||
for (int i = 0; i < list1.Count; i++)
|
||||
{
|
||||
hash = unchecked(hash + StringComparer.OrdinalIgnoreCase.GetHashCode(element));
|
||||
if (!string.Equals(list1.ElementAt(i), list2.ElementAt(i), StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int GetOrderIndependentHashCode<T>(IEnumerable<T> source) where T : notnull
|
||||
{
|
||||
int hash = 0;
|
||||
foreach (T element in source)
|
||||
{
|
||||
hash = unchecked(hash +
|
||||
EqualityComparer<T>.Default.GetHashCode(element));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.TextureCompression;
|
||||
using LightlessSync.WebAPI.Files;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -9,45 +7,24 @@ namespace LightlessSync.PlayerData.Factories;
|
||||
|
||||
public class FileDownloadManagerFactory
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly FileCompactor _fileCompactor;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly TextureDownscaleService _textureDownscaleService;
|
||||
private readonly TextureMetadataHelper _textureMetadataHelper;
|
||||
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
|
||||
public FileDownloadManagerFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
LightlessMediator lightlessMediator,
|
||||
FileTransferOrchestrator fileTransferOrchestrator,
|
||||
FileCacheManager fileCacheManager,
|
||||
FileCompactor fileCompactor,
|
||||
LightlessConfigService configService,
|
||||
TextureDownscaleService textureDownscaleService,
|
||||
TextureMetadataHelper textureMetadataHelper)
|
||||
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, LightlessMediator lightlessMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_fileTransferOrchestrator = fileTransferOrchestrator;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_fileCompactor = fileCompactor;
|
||||
_configService = configService;
|
||||
_textureDownscaleService = textureDownscaleService;
|
||||
_textureMetadataHelper = textureMetadataHelper;
|
||||
}
|
||||
|
||||
public FileDownloadManager Create()
|
||||
{
|
||||
return new FileDownloadManager(
|
||||
_loggerFactory.CreateLogger<FileDownloadManager>(),
|
||||
_lightlessMediator,
|
||||
_fileTransferOrchestrator,
|
||||
_fileCacheManager,
|
||||
_fileCompactor,
|
||||
_configService,
|
||||
_textureDownscaleService,
|
||||
_textureMetadataHelper);
|
||||
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _lightlessMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,40 +2,29 @@
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Factories;
|
||||
|
||||
public class GameObjectHandlerFactory
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||
|
||||
public GameObjectHandlerFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
PerformanceCollectorService performanceCollectorService,
|
||||
LightlessMediator lightlessMediator,
|
||||
IServiceProvider serviceProvider)
|
||||
public GameObjectHandlerFactory(ILoggerFactory loggerFactory, PerformanceCollectorService performanceCollectorService, LightlessMediator lightlessMediator,
|
||||
DalamudUtilService dalamudUtilService)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_performanceCollectorService = performanceCollectorService;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_serviceProvider = serviceProvider;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
}
|
||||
|
||||
public async Task<GameObjectHandler> Create(ObjectKind objectKind, Func<nint> getAddressFunc, bool isWatched = false)
|
||||
{
|
||||
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
||||
return await dalamudUtilService.RunOnFrameworkThread(() => new GameObjectHandler(
|
||||
_loggerFactory.CreateLogger<GameObjectHandler>(),
|
||||
_performanceCollectorService,
|
||||
_lightlessMediator,
|
||||
dalamudUtilService,
|
||||
objectKind,
|
||||
getAddressFunc,
|
||||
isWatched)).ConfigureAwait(false);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => new GameObjectHandler(_loggerFactory.CreateLogger<GameObjectHandler>(),
|
||||
_performanceCollectorService, _lightlessMediator, _dalamudUtilService, objectKind, getAddressFunc, isWatched)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +1,35 @@
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LightlessSync.WebAPI;
|
||||
|
||||
namespace LightlessSync.PlayerData.Factories;
|
||||
|
||||
public class PairFactory
|
||||
{
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly PairHandlerFactory _cachedPlayerFactory;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly Lazy<ServerConfigurationManager> _serverConfigurationManager;
|
||||
private readonly Lazy<ApiController> _apiController;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
public PairFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
PairLedger pairLedger,
|
||||
LightlessMediator lightlessMediator,
|
||||
Lazy<ServerConfigurationManager> serverConfigurationManager,
|
||||
Lazy<ApiController> apiController)
|
||||
public PairFactory(ILoggerFactory loggerFactory, PairHandlerFactory cachedPlayerFactory,
|
||||
LightlessMediator lightlessMediator, ServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_pairLedger = pairLedger;
|
||||
_cachedPlayerFactory = cachedPlayerFactory;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_apiController = apiController;
|
||||
}
|
||||
|
||||
public Pair Create(UserFullPairDto userPairDto)
|
||||
{
|
||||
return CreateInternal(userPairDto);
|
||||
return new Pair(_loggerFactory.CreateLogger<Pair>(), userPairDto, _cachedPlayerFactory, _lightlessMediator, _serverConfigurationManager);
|
||||
}
|
||||
|
||||
public Pair Create(UserPairDto userPairDto)
|
||||
{
|
||||
var full = new UserFullPairDto(
|
||||
userPairDto.User,
|
||||
userPairDto.IndividualPairStatus,
|
||||
new List<string>(),
|
||||
userPairDto.OwnPermissions,
|
||||
userPairDto.OtherPermissions);
|
||||
|
||||
return CreateInternal(full);
|
||||
return new Pair(_loggerFactory.CreateLogger<Pair>(), new(userPairDto.User, userPairDto.IndividualPairStatus, [], userPairDto.OwnPermissions, userPairDto.OtherPermissions),
|
||||
_cachedPlayerFactory, _lightlessMediator, _serverConfigurationManager);
|
||||
}
|
||||
|
||||
public Pair? Create(PairDisplayEntry entry)
|
||||
{
|
||||
var dto = new UserFullPairDto(
|
||||
entry.User,
|
||||
entry.PairStatus ?? IndividualPairStatus.None,
|
||||
entry.Groups.Select(g => g.Group.GID).Distinct(StringComparer.Ordinal).ToList(),
|
||||
entry.SelfPermissions,
|
||||
entry.OtherPermissions);
|
||||
|
||||
return CreateInternal(dto);
|
||||
}
|
||||
|
||||
public Pair? Create(PairUniqueIdentifier ident)
|
||||
{
|
||||
if (!_pairLedger.TryGetEntry(ident, out var entry) || entry is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Create(entry);
|
||||
}
|
||||
|
||||
private Pair CreateInternal(UserFullPairDto dto)
|
||||
{
|
||||
return new Pair(
|
||||
_loggerFactory.CreateLogger<Pair>(),
|
||||
dto,
|
||||
_pairLedger,
|
||||
_lightlessMediator,
|
||||
_serverConfigurationManager.Value,
|
||||
_apiController);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
LightlessSync/PlayerData/Factories/PairHandlerFactory.cs
Normal file
55
LightlessSync/PlayerData/Factories/PairHandlerFactory.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Factories;
|
||||
|
||||
public class PairHandlerFactory
|
||||
{
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly FileDownloadManagerFactory _fileDownloadManagerFactory;
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||
|
||||
public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager,
|
||||
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
||||
FileCacheManager fileCacheManager, LightlessMediator lightlessMediator, PlayerPerformanceService playerPerformanceService,
|
||||
PairProcessingLimiter pairProcessingLimiter,
|
||||
ServerConfigurationManager serverConfigManager)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
_ipcManager = ipcManager;
|
||||
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||
_hostApplicationLifetime = hostApplicationLifetime;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
}
|
||||
|
||||
public PairHandler Create(Pair pair)
|
||||
{
|
||||
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _gameObjectHandlerFactory,
|
||||
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
||||
_fileCacheManager, _lightlessMediator, _playerPerformanceService, _pairProcessingLimiter, _serverConfigManager);
|
||||
}
|
||||
}
|
||||
@@ -98,19 +98,7 @@ public class PlayerDataFactory
|
||||
|
||||
private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
||||
{
|
||||
if (playerPointer == IntPtr.Zero)
|
||||
return true;
|
||||
|
||||
var character = (Character*)playerPointer;
|
||||
|
||||
if (character == null)
|
||||
return true;
|
||||
|
||||
var gameObject = &character->GameObject;
|
||||
if (gameObject == null)
|
||||
return true;
|
||||
|
||||
return gameObject->DrawObject == null;
|
||||
return ((Character*)playerPointer)->GameObject.DrawObject == null;
|
||||
}
|
||||
|
||||
private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
|
||||
|
||||
@@ -5,7 +5,6 @@ using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
|
||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
|
||||
namespace LightlessSync.PlayerData.Handlers;
|
||||
@@ -95,7 +94,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
public DrawCondition CurrentDrawCondition { get; set; } = DrawCondition.None;
|
||||
public byte Gender { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public uint EntityId { get; private set; } = uint.MaxValue;
|
||||
public ObjectKind ObjectKind { get; }
|
||||
public byte RaceId { get; private set; }
|
||||
public byte TribeId { get; private set; }
|
||||
@@ -144,7 +142,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
{
|
||||
Address = IntPtr.Zero;
|
||||
DrawObjectAddress = IntPtr.Zero;
|
||||
EntityId = uint.MaxValue;
|
||||
_haltProcessing = false;
|
||||
}
|
||||
|
||||
@@ -174,16 +171,13 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
Address = _getAddress();
|
||||
if (Address != IntPtr.Zero)
|
||||
{
|
||||
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
||||
var drawObjAddr = (IntPtr)gameObject->DrawObject;
|
||||
var drawObjAddr = (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->DrawObject;
|
||||
DrawObjectAddress = drawObjAddr;
|
||||
EntityId = gameObject->EntityId;
|
||||
CurrentDrawCondition = DrawCondition.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawObjectAddress = IntPtr.Zero;
|
||||
EntityId = uint.MaxValue;
|
||||
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
||||
}
|
||||
|
||||
@@ -377,8 +371,8 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
{
|
||||
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
||||
if (DrawObjectAddress == IntPtr.Zero) return DrawCondition.DrawObjectZero;
|
||||
var visibilityFlags = ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags;
|
||||
if (visibilityFlags != VisibilityFlags.None) return DrawCondition.RenderFlags;
|
||||
var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags) != 0x0;
|
||||
if (renderFlags) return DrawCondition.RenderFlags;
|
||||
|
||||
if (ObjectKind == ObjectKind.Player)
|
||||
{
|
||||
|
||||
775
LightlessSync/PlayerData/Handlers/PairHandler.cs
Normal file
775
LightlessSync/PlayerData/Handlers/PairHandler.cs
Normal file
@@ -0,0 +1,775 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI.Files;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
|
||||
namespace LightlessSync.PlayerData.Handlers;
|
||||
|
||||
public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
|
||||
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly FileDownloadManager _downloadManager;
|
||||
private readonly FileCacheManager _fileDbManager;
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly IHostApplicationLifetime _lifetime;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
||||
private Guid _applicationId;
|
||||
private Task? _applicationTask;
|
||||
private CharacterData? _cachedData = null;
|
||||
private GameObjectHandler? _charaHandler;
|
||||
private readonly Dictionary<ObjectKind, Guid?> _customizeIds = [];
|
||||
private CombatData? _dataReceivedInDowntime;
|
||||
private CancellationTokenSource? _downloadCancellationTokenSource = new();
|
||||
private bool _forceApplyMods = false;
|
||||
private bool _isVisible;
|
||||
private Guid _penumbraCollection;
|
||||
private bool _redrawOnNextApplication = false;
|
||||
|
||||
public PairHandler(ILogger<PairHandler> logger, Pair pair,
|
||||
GameObjectHandlerFactory gameObjectHandlerFactory,
|
||||
IpcManager ipcManager, FileDownloadManager transferManager,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
||||
FileCacheManager fileDbManager, LightlessMediator mediator,
|
||||
PlayerPerformanceService playerPerformanceService,
|
||||
PairProcessingLimiter pairProcessingLimiter,
|
||||
ServerConfigurationManager serverConfigManager) : base(logger, mediator)
|
||||
{
|
||||
Pair = pair;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
_ipcManager = ipcManager;
|
||||
_downloadManager = transferManager;
|
||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_lifetime = lifetime;
|
||||
_fileDbManager = fileDbManager;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, Pair.UserData.UID).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
|
||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) =>
|
||||
{
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_charaHandler?.Invalidate();
|
||||
IsVisible = false;
|
||||
});
|
||||
Mediator.Subscribe<PenumbraInitializedMessage>(this, (_) =>
|
||||
{
|
||||
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, Pair.UserData.UID).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (!IsVisible && _charaHandler != null)
|
||||
{
|
||||
PlayerName = string.Empty;
|
||||
_charaHandler.Dispose();
|
||||
_charaHandler = null;
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<ClassJobChangedMessage>(this, (msg) =>
|
||||
{
|
||||
if (msg.GameObjectHandler == _charaHandler)
|
||||
{
|
||||
_redrawOnNextApplication = true;
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<CombatEndMessage>(this, (msg) =>
|
||||
{
|
||||
EnableSync();
|
||||
});
|
||||
Mediator.Subscribe<CombatStartMessage>(this, _ =>
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public bool IsVisible
|
||||
{
|
||||
get => _isVisible;
|
||||
private set
|
||||
{
|
||||
if (_isVisible != value)
|
||||
{
|
||||
_isVisible = value;
|
||||
string text = "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible");
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler),
|
||||
EventSeverity.Informational, text)));
|
||||
Mediator.Publish(new RefreshUiMessage());
|
||||
Mediator.Publish(new VisibilityChange());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long LastAppliedDataBytes { get; private set; }
|
||||
public Pair Pair { get; private set; }
|
||||
public nint PlayerCharacter => _charaHandler?.Address ?? nint.Zero;
|
||||
public unsafe uint PlayerCharacterId => (_charaHandler?.Address ?? nint.Zero) == nint.Zero
|
||||
? uint.MaxValue
|
||||
: ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_charaHandler!.Address)->EntityId;
|
||||
public string? PlayerName { get; private set; }
|
||||
public string PlayerNameHash => Pair.Ident;
|
||||
|
||||
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
||||
{
|
||||
if (_dalamudUtil.IsInCombat)
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||
"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;
|
||||
}
|
||||
|
||||
if (_charaHandler == null || (PlayerCharacter == IntPtr.Zero))
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||
"Cannot apply character data: Receiving Player is in an invalid state, deferring application")));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player was in invalid state, charaHandlerIsNull: {charaIsNull}, playerPointerIsNull: {ptrIsNull}",
|
||||
applicationBase, _charaHandler == null, PlayerCharacter == IntPtr.Zero);
|
||||
var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _cachedData, Logger,
|
||||
this, forceApplyCustomization, forceApplyMods: false)
|
||||
.Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles));
|
||||
_forceApplyMods = hasDiffMods || _forceApplyMods || (PlayerCharacter == IntPtr.Zero && _cachedData == null);
|
||||
_cachedData = characterData;
|
||||
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
|
||||
return;
|
||||
}
|
||||
|
||||
SetUploading(isUploading: false);
|
||||
|
||||
Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, this, forceApplyCustomization, _forceApplyMods);
|
||||
Logger.LogDebug("[BASE-{appbase}] Hash for data is {newHash}, current cache hash is {oldHash}", applicationBase, characterData.DataHash.Value, _cachedData?.DataHash.Value ?? "NODATA");
|
||||
|
||||
if (string.Equals(characterData.DataHash.Value, _cachedData?.DataHash.Value ?? string.Empty, StringComparison.Ordinal) && !forceApplyCustomization) return;
|
||||
|
||||
if (_dalamudUtil.IsInCutscene || _dalamudUtil.IsInGpose || !_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning,
|
||||
"Cannot apply character data: you are in GPose, a Cutscene or Penumbra/Glamourer is not available")));
|
||||
Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while in cutscene/gpose or Penumbra/Glamourer unavailable, returning", applicationBase, this);
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational,
|
||||
"Applying Character Data")));
|
||||
|
||||
_forceApplyMods |= forceApplyCustomization;
|
||||
|
||||
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods);
|
||||
|
||||
if (_charaHandler != null && _forceApplyMods)
|
||||
{
|
||||
_forceApplyMods = false;
|
||||
}
|
||||
|
||||
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
||||
{
|
||||
player.Add(PlayerChanges.ForcedRedraw);
|
||||
_redrawOnNextApplication = false;
|
||||
}
|
||||
|
||||
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
|
||||
{
|
||||
_pluginWarningNotificationManager.NotifyForMissingPlugins(Pair.UserData, PlayerName!, playerChanges);
|
||||
}
|
||||
|
||||
Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, this);
|
||||
|
||||
DownloadAndApplyCharacter(applicationBase, characterData.DeepClone(), charaDataToUpdate);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Pair == null
|
||||
? base.ToString() ?? string.Empty
|
||||
: Pair.UserData.AliasOrUID + ":" + PlayerName + ":" + (PlayerCharacter != nint.Zero ? "HasChar" : "NoChar");
|
||||
}
|
||||
|
||||
internal void SetUploading(bool isUploading = true)
|
||||
{
|
||||
Logger.LogTrace("Setting {this} uploading {uploading}", this, isUploading);
|
||||
if (_charaHandler != null)
|
||||
{
|
||||
Mediator.Publish(new PlayerUploadingMessage(_charaHandler, isUploading));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
SetUploading(isUploading: false);
|
||||
var name = PlayerName;
|
||||
Logger.LogDebug("Disposing {name} ({user})", name, Pair);
|
||||
try
|
||||
{
|
||||
Guid applicationId = Guid.NewGuid();
|
||||
_applicationCancellationTokenSource?.CancelDispose();
|
||||
_applicationCancellationTokenSource = null;
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
_downloadManager.Dispose();
|
||||
_charaHandler?.Dispose();
|
||||
_charaHandler = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(name, Pair.UserData, nameof(PairHandler), EventSeverity.Informational, "Disposing User")));
|
||||
}
|
||||
|
||||
if (_lifetime.ApplicationStopping.IsCancellationRequested) return;
|
||||
|
||||
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
|
||||
{
|
||||
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, Pair.UserPair);
|
||||
Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, Pair.UserPair);
|
||||
_ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).GetAwaiter().GetResult();
|
||||
if (!IsVisible)
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, Pair.UserPair);
|
||||
_ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult();
|
||||
}
|
||||
else
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(60));
|
||||
|
||||
Logger.LogInformation("[{applicationId}] CachedData is null {isNull}, contains things: {contains}", applicationId, _cachedData == null, _cachedData?.FileReplacements.Any() ?? false);
|
||||
|
||||
foreach (KeyValuePair<ObjectKind, List<FileReplacementData>> item in _cachedData?.FileReplacements ?? [])
|
||||
{
|
||||
try
|
||||
{
|
||||
RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed disposing player (not present anymore?)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Error on disposal of {name}", name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PlayerName = null;
|
||||
_cachedData = null;
|
||||
Logger.LogDebug("Disposing {name} complete", name);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyCustomizationDataAsync(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, CharacterData charaData, CancellationToken token)
|
||||
{
|
||||
if (PlayerCharacter == nint.Zero) return;
|
||||
var ptr = PlayerCharacter;
|
||||
|
||||
var handler = changes.Key switch
|
||||
{
|
||||
ObjectKind.Player => _charaHandler!,
|
||||
ObjectKind.Companion => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetCompanionPtr(ptr), isWatched: false).ConfigureAwait(false),
|
||||
ObjectKind.MinionOrMount => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetMinionOrMountPtr(ptr), isWatched: false).ConfigureAwait(false),
|
||||
ObjectKind.Pet => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPetPtr(ptr), isWatched: false).ConfigureAwait(false),
|
||||
_ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
if (handler.Address == nint.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
foreach (var change in changes.Value.OrderBy(p => (int)p))
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
|
||||
switch (change)
|
||||
{
|
||||
case PlayerChanges.Customize:
|
||||
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
|
||||
{
|
||||
_customizeIds[changes.Key] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(handler.Address, customizePlusData).ConfigureAwait(false);
|
||||
}
|
||||
else if (_customizeIds.TryGetValue(changes.Key, out var customizeId))
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
_customizeIds.Remove(changes.Key);
|
||||
}
|
||||
break;
|
||||
|
||||
case PlayerChanges.Heels:
|
||||
await _ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.Honorific:
|
||||
await _ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.Glamourer:
|
||||
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
|
||||
{
|
||||
await _ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token).ConfigureAwait(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case PlayerChanges.Moodles:
|
||||
await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.PetNames:
|
||||
await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.ForcedRedraw:
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (handler != _charaHandler) handler.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void DownloadAndApplyCharacter(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData)
|
||||
{
|
||||
if (!updatedData.Any())
|
||||
{
|
||||
Logger.LogDebug("[BASE-{appBase}] Nothing to update for {obj}", applicationBase, this);
|
||||
return;
|
||||
}
|
||||
|
||||
var updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModFiles));
|
||||
var updateManip = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModManip));
|
||||
|
||||
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
|
||||
var downloadToken = _downloadCancellationTokenSource.Token;
|
||||
|
||||
_ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, downloadToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task? _pairDownloadTask;
|
||||
|
||||
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||
bool updateModdedPaths, bool updateManip, CancellationToken downloadToken)
|
||||
{
|
||||
await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
|
||||
|
||||
if (updateModdedPaths)
|
||||
{
|
||||
int attempts = 0;
|
||||
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||
|
||||
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
||||
{
|
||||
if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted)
|
||||
{
|
||||
Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData);
|
||||
await _pairDownloadTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData);
|
||||
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational,
|
||||
$"Starting download for {toDownloadReplacements.Count} files")));
|
||||
var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false);
|
||||
|
||||
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles))
|
||||
{
|
||||
_downloadManager.ClearDownload();
|
||||
return;
|
||||
}
|
||||
|
||||
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false));
|
||||
|
||||
await _pairDownloadTask.ConfigureAwait(false);
|
||||
|
||||
if (downloadToken.IsCancellationRequested)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase);
|
||||
return;
|
||||
}
|
||||
|
||||
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||
|
||||
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false))
|
||||
return;
|
||||
}
|
||||
|
||||
downloadToken.ThrowIfCancellationRequested();
|
||||
|
||||
var appToken = _applicationCancellationTokenSource?.Token;
|
||||
while ((!_applicationTask?.IsCompleted ?? false)
|
||||
&& !downloadToken.IsCancellationRequested
|
||||
&& (!appToken?.IsCancellationRequested ?? false))
|
||||
{
|
||||
// block until current application is done
|
||||
Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName);
|
||||
await Task.Delay(250).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false)) return;
|
||||
|
||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
||||
var token = _applicationCancellationTokenSource.Token;
|
||||
|
||||
_applicationTask = ApplyCharacterDataAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
|
||||
}
|
||||
|
||||
private async Task ApplyCharacterDataAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
_applicationId = Guid.NewGuid();
|
||||
Logger.LogDebug("[BASE-{applicationId}] Starting application task for {this}: {appId}", applicationBase, this, _applicationId);
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, _charaHandler);
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, _charaHandler!, _applicationId, 30000, token).ConfigureAwait(false);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (updateModdedPaths)
|
||||
{
|
||||
// ensure collection is set
|
||||
var objIndex = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler!.GetGameObject()!.ObjectIndex).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, objIndex).ConfigureAwait(false);
|
||||
|
||||
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, _penumbraCollection,
|
||||
moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false);
|
||||
LastAppliedDataBytes = -1;
|
||||
foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists))
|
||||
{
|
||||
if (LastAppliedDataBytes == -1) LastAppliedDataBytes = 0;
|
||||
|
||||
LastAppliedDataBytes += path.Length;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateManip)
|
||||
{
|
||||
await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, _applicationId, _penumbraCollection, charaData.ManipulationData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var kind in updatedData)
|
||||
{
|
||||
await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
_cachedData = charaData;
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
|
||||
{
|
||||
IsVisible = false;
|
||||
_forceApplyMods = true;
|
||||
_cachedData = charaData;
|
||||
Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FrameworkUpdate()
|
||||
{
|
||||
if (string.IsNullOrEmpty(PlayerName))
|
||||
{
|
||||
var pc = _dalamudUtil.FindPlayerByNameHash(Pair.Ident);
|
||||
if (pc == default((string, nint))) return;
|
||||
Logger.LogDebug("One-Time Initializing {this}", this);
|
||||
Initialize(pc.Name);
|
||||
Logger.LogDebug("One-Time Initialized {this}", this);
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Informational,
|
||||
$"Initializing User For Character {pc.Name}")));
|
||||
}
|
||||
|
||||
if (_charaHandler?.Address != nint.Zero && !IsVisible)
|
||||
{
|
||||
Guid appData = Guid.NewGuid();
|
||||
IsVisible = true;
|
||||
if (_cachedData != null)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] {this} visibility changed, now: {visi}, cached data exists", appData, this, IsVisible);
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
ApplyCharacterData(appData, _cachedData!, forceApplyCustomization: true);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogTrace("{this} visibility changed, now: {visi}, no cached data exists", this, IsVisible);
|
||||
}
|
||||
}
|
||||
else if (_charaHandler?.Address == nint.Zero && IsVisible)
|
||||
{
|
||||
IsVisible = false;
|
||||
_charaHandler.Invalidate();
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
Logger.LogTrace("{this} visibility changed, now: {visi}", this, IsVisible);
|
||||
}
|
||||
}
|
||||
|
||||
private void Initialize(string name)
|
||||
{
|
||||
PlayerName = name;
|
||||
_charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Pair.Ident), isWatched: false).GetAwaiter().GetResult();
|
||||
|
||||
_serverConfigManager.AutoPopulateNoteForUid(Pair.UserData.UID, name);
|
||||
|
||||
Mediator.Subscribe<HonorificReadyMessage>(this, async (_) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(_cachedData?.HonorificData)) return;
|
||||
Logger.LogTrace("Reapplying Honorific data for {this}", this);
|
||||
await _ipcManager.Honorific.SetTitleAsync(PlayerCharacter, _cachedData.HonorificData).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
Mediator.Subscribe<PetNamesReadyMessage>(this, async (_) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(_cachedData?.PetNamesData)) return;
|
||||
Logger.LogTrace("Reapplying Pet Names data for {this}", this);
|
||||
await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, _cachedData.PetNamesData).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
_ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, _charaHandler.GetGameObject()!.ObjectIndex).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId, CancellationToken cancelToken)
|
||||
{
|
||||
nint address = _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Pair.Ident);
|
||||
if (address == nint.Zero) return;
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, Pair.UserData.AliasOrUID, name, objectKind);
|
||||
|
||||
if (_customizeIds.TryGetValue(objectKind, out var customizeId))
|
||||
{
|
||||
_customizeIds.Remove(objectKind);
|
||||
}
|
||||
|
||||
if (objectKind == ObjectKind.Player)
|
||||
{
|
||||
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, () => address, isWatched: false).ConfigureAwait(false);
|
||||
tempHandler.CompareNameAndThrow(name);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Customization and Equipment for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
tempHandler.CompareNameAndThrow(name);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.Heels.RestoreOffsetForPlayerAsync(address).ConfigureAwait(false);
|
||||
tempHandler.CompareNameAndThrow(name);
|
||||
Logger.LogDebug("[{applicationId}] Restoring C+ for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
tempHandler.CompareNameAndThrow(name);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Honorific for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.Honorific.ClearTitleAsync(address).ConfigureAwait(false);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Moodles for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.Moodles.RevertStatusAsync(address).ConfigureAwait(false);
|
||||
Logger.LogDebug("[{applicationId}] Restoring Pet Nicknames for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name);
|
||||
await _ipcManager.PetNames.ClearPlayerData(address).ConfigureAwait(false);
|
||||
}
|
||||
else if (objectKind == ObjectKind.MinionOrMount)
|
||||
{
|
||||
var minionOrMount = await _dalamudUtil.GetMinionOrMountAsync(address).ConfigureAwait(false);
|
||||
if (minionOrMount != nint.Zero)
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => minionOrMount, isWatched: false).ConfigureAwait(false);
|
||||
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (objectKind == ObjectKind.Pet)
|
||||
{
|
||||
var pet = await _dalamudUtil.GetPetAsync(address).ConfigureAwait(false);
|
||||
if (pet != nint.Zero)
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => pet, isWatched: false).ConfigureAwait(false);
|
||||
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (objectKind == ObjectKind.Companion)
|
||||
{
|
||||
var companion = await _dalamudUtil.GetCompanionAsync(address).ConfigureAwait(false);
|
||||
if (companion != nint.Zero)
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
using GameObjectHandler tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Pet, () => companion, isWatched: false).ConfigureAwait(false);
|
||||
await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<FileReplacementData> TryCalculateModdedDictionary(Guid applicationBase, CharacterData charaData, out Dictionary<(string GamePath, string? Hash), string> moddedDictionary, CancellationToken token)
|
||||
{
|
||||
Stopwatch st = Stopwatch.StartNew();
|
||||
ConcurrentBag<FileReplacementData> missingFiles = [];
|
||||
moddedDictionary = [];
|
||||
ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new();
|
||||
bool hasMigrationChanges = false;
|
||||
|
||||
try
|
||||
{
|
||||
var replacementList = charaData.FileReplacements.SelectMany(k => k.Value.Where(v => string.IsNullOrEmpty(v.FileSwapPath))).ToList();
|
||||
Parallel.ForEach(replacementList, new ParallelOptions()
|
||||
{
|
||||
CancellationToken = token,
|
||||
MaxDegreeOfParallelism = 4
|
||||
},
|
||||
(item) =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var fileCache = _fileDbManager.GetFileCacheByHash(item.Hash);
|
||||
if (fileCache != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(new FileInfo(fileCache.ResolvedFilepath).Extension))
|
||||
{
|
||||
hasMigrationChanges = true;
|
||||
fileCache = _fileDbManager.MigrateFileHashToExtension(fileCache, item.GamePaths[0].Split(".")[^1]);
|
||||
}
|
||||
|
||||
foreach (var gamePath in item.GamePaths)
|
||||
{
|
||||
outputDict[(gamePath, item.Hash)] = fileCache.ResolvedFilepath;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogTrace("Missing file: {hash}", item.Hash);
|
||||
missingFiles.Add(item);
|
||||
}
|
||||
});
|
||||
|
||||
moddedDictionary = outputDict.ToDictionary(k => k.Key, k => k.Value);
|
||||
|
||||
foreach (var item in charaData.FileReplacements.SelectMany(k => k.Value.Where(v => !string.IsNullOrEmpty(v.FileSwapPath))).ToList())
|
||||
{
|
||||
foreach (var gamePath in item.GamePaths)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] Adding file swap for {path}: {fileSwap}", applicationBase, gamePath, item.FileSwapPath);
|
||||
moddedDictionary[(gamePath, null)] = item.FileSwapPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] Modded path calculation cancelled", applicationBase);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "[BASE-{appBase}] Something went wrong during calculation replacements", applicationBase);
|
||||
}
|
||||
if (hasMigrationChanges) _fileDbManager.WriteOutFullCsv();
|
||||
st.Stop();
|
||||
Logger.LogDebug("[BASE-{appBase}] ModdedPaths calculated in {time}ms, missing files: {count}, total files: {total}", applicationBase, st.ElapsedMilliseconds, missingFiles.Count, moddedDictionary.Keys.Count);
|
||||
return [.. missingFiles];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using LightlessSync.API.Data;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// orchestrates the lifecycle of a paired character
|
||||
/// </summary>
|
||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||
{
|
||||
new string Ident { get; }
|
||||
bool Initialized { get; }
|
||||
bool IsVisible { get; }
|
||||
bool ScheduledForDeletion { get; set; }
|
||||
CharacterData? LastReceivedCharacterData { get; }
|
||||
long LastAppliedDataBytes { get; }
|
||||
new string? PlayerName { get; }
|
||||
string PlayerNameHash { get; }
|
||||
uint PlayerCharacterId { get; }
|
||||
DateTime? LastDataReceivedAt { get; }
|
||||
DateTime? LastApplyAttemptAt { get; }
|
||||
DateTime? LastSuccessfulApplyAt { get; }
|
||||
string? LastFailureReason { get; }
|
||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||
bool IsApplying { get; }
|
||||
bool IsDownloading { get; }
|
||||
int PendingDownloadCount { get; }
|
||||
int ForbiddenDownloadCount { get; }
|
||||
|
||||
void Initialize();
|
||||
void ApplyData(CharacterData data);
|
||||
void ApplyLastReceivedData(bool forced = false);
|
||||
bool FetchPerformanceMetricsFromCache();
|
||||
void LoadCachedCharacterData(CharacterData data);
|
||||
void SetUploading(bool uploading);
|
||||
void SetPaused(bool paused);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
public interface IPairHandlerAdapterFactory
|
||||
{
|
||||
IPairHandlerAdapter Create(string ident);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using LightlessSync.API.Data;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// performance metrics for each pair handler
|
||||
/// </summary>
|
||||
public interface IPairPerformanceSubject
|
||||
{
|
||||
string Ident { get; }
|
||||
string PlayerName { get; }
|
||||
UserData UserData { get; }
|
||||
bool IsPaused { get; }
|
||||
bool IsDirectlyPaired { get; }
|
||||
bool HasStickyPermissions { get; }
|
||||
long LastAppliedApproximateVRAMBytes { get; set; }
|
||||
long LastAppliedApproximateEffectiveVRAMBytes { get; set; }
|
||||
long LastAppliedDataTris { get; set; }
|
||||
}
|
||||
10
LightlessSync/PlayerData/Pairs/OptionalPluginWarning.cs
Normal file
10
LightlessSync/PlayerData/Pairs/OptionalPluginWarning.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
public record OptionalPluginWarning
|
||||
{
|
||||
public bool ShownHeelsWarning { get; set; } = false;
|
||||
public bool ShownCustomizePlusWarning { get; set; } = false;
|
||||
public bool ShownHonorificWarning { get; set; } = false;
|
||||
public bool ShownMoodlesWarning { get; set; } = false;
|
||||
public bool ShowPetNicknamesWarning { get; set; } = false;
|
||||
}
|
||||
@@ -1,156 +1,173 @@
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.WebAPI;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// ui wrapper around a pair connection
|
||||
/// </summary>
|
||||
public class Pair
|
||||
{
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly PairHandlerFactory _cachedPlayerFactory;
|
||||
private readonly SemaphoreSlim _creationSemaphore = new(1);
|
||||
private readonly ILogger<Pair> _logger;
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly Lazy<ApiController> _apiController;
|
||||
private CancellationTokenSource _applicationCts = new();
|
||||
private OnlineUserIdentDto? _onlineUserIdentDto = null;
|
||||
|
||||
private const int _lightlessPrefixColor = 708;
|
||||
|
||||
public Pair(
|
||||
ILogger<Pair> logger,
|
||||
UserFullPairDto userPair,
|
||||
PairLedger pairLedger,
|
||||
LightlessMediator mediator,
|
||||
ServerConfigurationManager serverConfigurationManager,
|
||||
Lazy<ApiController> apiController)
|
||||
public Pair(ILogger<Pair> logger, UserFullPairDto userPair, PairHandlerFactory cachedPlayerFactory,
|
||||
LightlessMediator mediator, ServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_logger = logger;
|
||||
UserPair = userPair;
|
||||
_pairLedger = pairLedger;
|
||||
_cachedPlayerFactory = cachedPlayerFactory;
|
||||
_mediator = mediator;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_apiController = apiController;
|
||||
}
|
||||
|
||||
private PairUniqueIdentifier PairIdent => UniqueIdent;
|
||||
|
||||
private IPairHandlerAdapter? TryGetHandler()
|
||||
{
|
||||
return _pairLedger.GetHandler(PairIdent);
|
||||
}
|
||||
|
||||
private PairConnection? TryGetConnection()
|
||||
{
|
||||
return _pairLedger.TryGetEntry(PairIdent, out var entry) && entry is not null
|
||||
? entry.Connection
|
||||
: null;
|
||||
}
|
||||
|
||||
public bool HasCachedPlayer => TryGetHandler() is not null;
|
||||
public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName) && _onlineUserIdentDto != null;
|
||||
public IndividualPairStatus IndividualPairStatus => UserPair.IndividualPairStatus;
|
||||
public bool IsDirectlyPaired => IndividualPairStatus != IndividualPairStatus.None;
|
||||
public bool IsOneSidedPair => IndividualPairStatus == IndividualPairStatus.OneSided;
|
||||
|
||||
public bool IsOnline => TryGetConnection()?.IsOnline ?? false;
|
||||
public bool IsOnline => CachedPlayer != null;
|
||||
|
||||
public bool IsPaired => IndividualPairStatus == IndividualPairStatus.Bidirectional || UserPair.Groups.Any();
|
||||
public bool IsPaused => UserPair.OwnPermissions.IsPaused();
|
||||
public bool IsVisible => _pairLedger.IsPairVisible(PairIdent);
|
||||
public CharacterData? LastReceivedCharacterData => TryGetHandler()?.LastReceivedCharacterData;
|
||||
public string? PlayerName => TryGetHandler()?.PlayerName ?? UserPair.User.AliasOrUID;
|
||||
public long LastAppliedDataBytes => TryGetHandler()?.LastAppliedDataBytes ?? -1;
|
||||
public long LastAppliedDataTris => TryGetHandler()?.LastAppliedDataTris ?? -1;
|
||||
public long LastAppliedApproximateVRAMBytes => TryGetHandler()?.LastAppliedApproximateVRAMBytes ?? -1;
|
||||
public long LastAppliedApproximateEffectiveVRAMBytes => TryGetHandler()?.LastAppliedApproximateEffectiveVRAMBytes ?? -1;
|
||||
public string Ident => TryGetHandler()?.Ident ?? TryGetConnection()?.Ident ?? string.Empty;
|
||||
public uint PlayerCharacterId => TryGetHandler()?.PlayerCharacterId ?? uint.MaxValue;
|
||||
public PairUniqueIdentifier UniqueIdent => new(UserData.UID);
|
||||
public bool IsVisible => CachedPlayer?.IsVisible ?? false;
|
||||
public CharacterData? LastReceivedCharacterData { get; set; }
|
||||
public string? PlayerName => CachedPlayer?.PlayerName ?? string.Empty;
|
||||
public long LastAppliedDataBytes => CachedPlayer?.LastAppliedDataBytes ?? -1;
|
||||
public long LastAppliedDataTris { get; set; } = -1;
|
||||
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
||||
public string Ident => _onlineUserIdentDto?.Ident ?? string.Empty;
|
||||
public uint PlayerCharacterId => CachedPlayer?.PlayerCharacterId ?? uint.MaxValue;
|
||||
|
||||
public UserData UserData => UserPair.User;
|
||||
|
||||
public UserFullPairDto UserPair { get; set; }
|
||||
private PairHandler? CachedPlayer { get; set; }
|
||||
|
||||
public void AddContextMenu(IMenuOpenedArgs args)
|
||||
{
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (CachedPlayer == null || (args.Target is not MenuTargetDefault target) || target.TargetObjectId != CachedPlayer.PlayerCharacterId || IsPaused) return;
|
||||
|
||||
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId || IsPaused)
|
||||
SeStringBuilder seStringBuilder = new();
|
||||
SeStringBuilder seStringBuilder2 = new();
|
||||
SeStringBuilder seStringBuilder3 = new();
|
||||
SeStringBuilder seStringBuilder4 = new();
|
||||
var openProfileSeString = seStringBuilder.AddText("Open Profile").Build();
|
||||
var reapplyDataSeString = seStringBuilder2.AddText("Reapply last data").Build();
|
||||
var cyclePauseState = seStringBuilder3.AddText("Cycle pause state").Build();
|
||||
var changePermissions = seStringBuilder4.AddText("Change Permissions").Build();
|
||||
args.AddMenuItem(new MenuItem()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||
return Task.CompletedTask;
|
||||
Name = openProfileSeString,
|
||||
OnClicked = (a) => _mediator.Publish(new ProfileOpenStandaloneMessage(this)),
|
||||
UseDefaultPrefix = false,
|
||||
PrefixChar = 'L',
|
||||
PrefixColor = 708
|
||||
});
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
args.AddMenuItem(new MenuItem()
|
||||
{
|
||||
ApplyLastReceivedData(forced: true);
|
||||
return Task.CompletedTask;
|
||||
Name = reapplyDataSeString,
|
||||
OnClicked = (a) => ApplyLastReceivedData(forced: true),
|
||||
UseDefaultPrefix = false,
|
||||
PrefixChar = 'L',
|
||||
PrefixColor = 708
|
||||
});
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
args.AddMenuItem(new MenuItem()
|
||||
{
|
||||
_mediator.Publish(new OpenPermissionWindow(this));
|
||||
return Task.CompletedTask;
|
||||
Name = changePermissions,
|
||||
OnClicked = (a) => _mediator.Publish(new OpenPermissionWindow(this)),
|
||||
UseDefaultPrefix = false,
|
||||
PrefixChar = 'L',
|
||||
PrefixColor = 708
|
||||
});
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Cycle pause state", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
args.AddMenuItem(new MenuItem()
|
||||
{
|
||||
TriggerCyclePause();
|
||||
return Task.CompletedTask;
|
||||
Name = cyclePauseState,
|
||||
OnClicked = (a) => _mediator.Publish(new CyclePauseMessage(UserData)),
|
||||
UseDefaultPrefix = false,
|
||||
PrefixChar = 'L',
|
||||
PrefixColor = 708
|
||||
});
|
||||
}
|
||||
|
||||
public void ApplyData(OnlineUserCharaDataDto data)
|
||||
{
|
||||
_logger.LogTrace("Character data received for {Uid}; handler will process via registry.", UserData.UID);
|
||||
}
|
||||
_applicationCts = _applicationCts.CancelRecreate();
|
||||
LastReceivedCharacterData = data.CharaData;
|
||||
|
||||
private void TriggerCyclePause()
|
||||
{
|
||||
_ = _apiController.Value.CyclePauseAsync(this);
|
||||
if (CachedPlayer == null)
|
||||
{
|
||||
_logger.LogDebug("Received Data for {uid} but CachedPlayer does not exist, waiting", data.User.UID);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
using var timeoutCts = new CancellationTokenSource();
|
||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(120));
|
||||
var appToken = _applicationCts.Token;
|
||||
using var combined = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, appToken);
|
||||
while (CachedPlayer == null && !combined.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(250, combined.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!combined.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogDebug("Applying delayed data for {uid}", data.User.UID);
|
||||
ApplyLastReceivedData();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyLastReceivedData();
|
||||
}
|
||||
|
||||
public void ApplyLastReceivedData(bool forced = false)
|
||||
{
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
{
|
||||
_logger.LogTrace("ApplyLastReceivedData skipped for {Uid}: handler missing.", UserData.UID);
|
||||
return;
|
||||
}
|
||||
if (CachedPlayer == null) return;
|
||||
if (LastReceivedCharacterData == null) return;
|
||||
|
||||
handler.ApplyLastReceivedData(forced);
|
||||
CachedPlayer.ApplyCharacterData(Guid.NewGuid(), RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, forced);
|
||||
}
|
||||
|
||||
public void CreateCachedPlayer(OnlineUserIdentDto? dto = null)
|
||||
{
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
try
|
||||
{
|
||||
_logger.LogTrace("CreateCachedPlayer skipped for {Uid}: handler unavailable.", UserData.UID);
|
||||
return;
|
||||
}
|
||||
_creationSemaphore.Wait();
|
||||
|
||||
if (!handler.Initialized)
|
||||
if (CachedPlayer != null) return;
|
||||
|
||||
if (dto == null && _onlineUserIdentDto == null)
|
||||
{
|
||||
CachedPlayer?.Dispose();
|
||||
CachedPlayer = null;
|
||||
return;
|
||||
}
|
||||
if (dto != null)
|
||||
{
|
||||
_onlineUserIdentDto = dto;
|
||||
}
|
||||
|
||||
CachedPlayer?.Dispose();
|
||||
CachedPlayer = _cachedPlayerFactory.Create(this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
handler.Initialize();
|
||||
_creationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +178,7 @@ public class Pair
|
||||
|
||||
public string GetPlayerNameHash()
|
||||
{
|
||||
return TryGetHandler()?.PlayerNameHash ?? string.Empty;
|
||||
return CachedPlayer?.PlayerNameHash ?? string.Empty;
|
||||
}
|
||||
|
||||
public bool HasAnyConnection()
|
||||
@@ -171,7 +188,21 @@ public class Pair
|
||||
|
||||
public void MarkOffline(bool wait = true)
|
||||
{
|
||||
_logger.LogTrace("MarkOffline invoked for {Uid} (wait: {Wait}). New registry handles handler disposal.", UserData.UID, wait);
|
||||
try
|
||||
{
|
||||
if (wait)
|
||||
_creationSemaphore.Wait();
|
||||
LastReceivedCharacterData = null;
|
||||
var player = CachedPlayer;
|
||||
CachedPlayer = null;
|
||||
player?.Dispose();
|
||||
_onlineUserIdentDto = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (wait)
|
||||
_creationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetNote(string note)
|
||||
@@ -181,36 +212,47 @@ public class Pair
|
||||
|
||||
internal void SetIsUploading()
|
||||
{
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
handler.SetUploading(true);
|
||||
CachedPlayer?.SetUploading();
|
||||
}
|
||||
|
||||
public PairDebugInfo GetDebugInfo()
|
||||
private CharacterData? RemoveNotSyncedFiles(CharacterData? data)
|
||||
{
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
_logger.LogTrace("Removing not synced files");
|
||||
if (data == null)
|
||||
{
|
||||
return PairDebugInfo.Empty;
|
||||
_logger.LogTrace("Nothing to remove");
|
||||
return data;
|
||||
}
|
||||
|
||||
return new PairDebugInfo(
|
||||
true,
|
||||
handler.Initialized,
|
||||
handler.IsVisible,
|
||||
handler.ScheduledForDeletion,
|
||||
handler.LastDataReceivedAt,
|
||||
handler.LastApplyAttemptAt,
|
||||
handler.LastSuccessfulApplyAt,
|
||||
handler.LastFailureReason,
|
||||
handler.LastBlockingConditions,
|
||||
handler.IsApplying,
|
||||
handler.IsDownloading,
|
||||
handler.PendingDownloadCount,
|
||||
handler.ForbiddenDownloadCount);
|
||||
bool disableIndividualAnimations = (UserPair.OtherPermissions.IsDisableAnimations() || UserPair.OwnPermissions.IsDisableAnimations());
|
||||
bool disableIndividualVFX = (UserPair.OtherPermissions.IsDisableVFX() || UserPair.OwnPermissions.IsDisableVFX());
|
||||
bool disableIndividualSounds = (UserPair.OtherPermissions.IsDisableSounds() || UserPair.OwnPermissions.IsDisableSounds());
|
||||
|
||||
_logger.LogTrace("Disable: Sounds: {disableIndividualSounds}, Anims: {disableIndividualAnims}; " +
|
||||
"VFX: {disableGroupSounds}",
|
||||
disableIndividualSounds, disableIndividualAnimations, disableIndividualVFX);
|
||||
|
||||
if (disableIndividualAnimations || disableIndividualSounds || disableIndividualVFX)
|
||||
{
|
||||
_logger.LogTrace("Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}, VFX disabled: {disableVFX}",
|
||||
disableIndividualAnimations, disableIndividualSounds, disableIndividualVFX);
|
||||
foreach (var objectKind in data.FileReplacements.Select(k => k.Key))
|
||||
{
|
||||
if (disableIndividualSounds)
|
||||
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||
.Where(f => !f.GamePaths.Any(p => p.EndsWith("scd", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
if (disableIndividualAnimations)
|
||||
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||
.Where(f => !f.GamePaths.Any(p => p.EndsWith("tmb", StringComparison.OrdinalIgnoreCase) || p.EndsWith("pap", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
if (disableIndividualVFX)
|
||||
data.FileReplacements[objectKind] = data.FileReplacements[objectKind]
|
||||
.Where(f => !f.GamePaths.Any(p => p.EndsWith("atex", StringComparison.OrdinalIgnoreCase) || p.EndsWith("avfx", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// handles group related pair events
|
||||
/// </summary>
|
||||
public sealed partial class PairCoordinator
|
||||
{
|
||||
public void HandleGroupChangePermissions(GroupPermissionDto dto)
|
||||
{
|
||||
var result = _pairManager.UpdateGroupPermissions(dto);
|
||||
if (!result.Success)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update permissions for group {GroupId}: {Error}", dto.Group.GID, result.Error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
|
||||
public void HandleGroupFullInfo(GroupFullInfoDto dto)
|
||||
{
|
||||
var result = _pairManager.AddGroup(dto);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to add group {GroupId}: {Error}", dto.Group.GID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
|
||||
public void HandleGroupPairJoined(GroupPairFullInfoDto dto)
|
||||
{
|
||||
var result = _pairManager.AddOrUpdateGroupPair(dto);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to add group pair {Uid}/{Group}: {Error}", dto.User.UID, dto.Group.GID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
|
||||
public void HandleGroupPairLeft(GroupPairDto dto)
|
||||
{
|
||||
var deregistration = _pairManager.RemoveGroupPair(dto);
|
||||
if (deregistration.Success && deregistration.Value is { } registration && registration.CharacterIdent is not null)
|
||||
{
|
||||
_ = _handlerRegistry.DeregisterOfflinePair(registration, forceDisposal: true);
|
||||
}
|
||||
else if (!deregistration.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("RemoveGroupPair failed for {Uid}: {Error}", dto.User.UID, deregistration.Error);
|
||||
}
|
||||
|
||||
if (deregistration.Success)
|
||||
{
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleGroupRemoved(GroupDto dto)
|
||||
{
|
||||
var removalResult = _pairManager.RemoveGroup(dto.Group.GID);
|
||||
if (removalResult.Success)
|
||||
{
|
||||
foreach (var registration in removalResult.Value)
|
||||
{
|
||||
if (registration.CharacterIdent is not null)
|
||||
{
|
||||
_ = _handlerRegistry.DeregisterOfflinePair(registration, forceDisposal: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to remove group {Group}: {Error}", dto.Group.GID, removalResult.Error);
|
||||
}
|
||||
|
||||
if (removalResult.Success)
|
||||
{
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleGroupInfoUpdate(GroupInfoDto dto)
|
||||
{
|
||||
var result = _pairManager.UpdateGroupInfo(dto);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update group info for {Group}: {Error}", dto.Group.GID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
|
||||
public void HandleGroupPairPermissions(GroupPairUserPermissionDto dto)
|
||||
{
|
||||
var result = _pairManager.UpdateGroupPairPermissions(dto);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update group pair permissions for {Group}: {Error}", dto.Group.GID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
|
||||
public void HandleGroupPairStatus(GroupPairUserInfoDto dto, bool isSelf)
|
||||
{
|
||||
PairOperationResult result;
|
||||
if (isSelf)
|
||||
{
|
||||
result = _pairManager.UpdateGroupStatus(dto);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = _pairManager.UpdateGroupPairStatus(dto);
|
||||
}
|
||||
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update group status for {Group}:{Uid}: {Error}", dto.GID, dto.UID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// handles user pair events
|
||||
/// </summary>
|
||||
public sealed partial class PairCoordinator
|
||||
{
|
||||
public void HandleUserAddPair(UserPairDto dto, bool addToLastAddedUser = true)
|
||||
{
|
||||
var result = _pairManager.AddOrUpdateIndividual(dto, addToLastAddedUser);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to add/update pair {Uid}: {Error}", dto.User.UID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
|
||||
public void HandleUserAddPair(UserFullPairDto dto)
|
||||
{
|
||||
var result = _pairManager.AddOrUpdateIndividual(dto);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to add/update full pair {Uid}: {Error}", dto.User.UID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
|
||||
public void HandleUserRemovePair(UserDto dto)
|
||||
{
|
||||
var removal = _pairManager.RemoveIndividual(dto);
|
||||
if (removal.Success && removal.Value is { } registration && registration.CharacterIdent is not null)
|
||||
{
|
||||
_ = _handlerRegistry.DeregisterOfflinePair(registration, forceDisposal: true);
|
||||
}
|
||||
else if (!removal.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("RemoveIndividual failed for {Uid}: {Error}", dto.User.UID, removal.Error);
|
||||
}
|
||||
|
||||
if (removal.Success)
|
||||
{
|
||||
_pendingCharacterData.TryRemove(dto.User.UID, out _);
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleUserStatus(UserIndividualPairStatusDto dto)
|
||||
{
|
||||
var result = _pairManager.SetIndividualStatus(dto);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update individual pair status for {Uid}: {Error}", dto.User.UID, result.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
|
||||
public void HandleUserOnline(OnlineUserIdentDto dto, bool sendNotification)
|
||||
{
|
||||
var wasOnline = false;
|
||||
PairConnection? previousConnection = null;
|
||||
if (_pairManager.TryGetPair(dto.User.UID, out var existingConnection))
|
||||
{
|
||||
previousConnection = existingConnection;
|
||||
wasOnline = existingConnection.IsOnline;
|
||||
}
|
||||
|
||||
var registrationResult = _pairManager.MarkOnline(dto);
|
||||
if (!registrationResult.Success)
|
||||
{
|
||||
_logger.LogDebug("MarkOnline failed for {Uid}: {Error}", dto.User.UID, registrationResult.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var registration = registrationResult.Value;
|
||||
if (registration.CharacterIdent is null)
|
||||
{
|
||||
_logger.LogDebug("Online registration for {Uid} missing ident.", dto.User.UID);
|
||||
}
|
||||
else
|
||||
{
|
||||
var handlerResult = _handlerRegistry.RegisterOnlinePair(registration);
|
||||
if (!handlerResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("RegisterOnlinePair failed for {Uid}: {Error}", dto.User.UID, handlerResult.Error);
|
||||
}
|
||||
}
|
||||
|
||||
var connectionResult = _pairManager.GetPair(dto.User.UID);
|
||||
var connection = connectionResult.Success ? connectionResult.Value : previousConnection;
|
||||
if (connection is not null)
|
||||
{
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(connection.User));
|
||||
}
|
||||
else
|
||||
{
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||
}
|
||||
|
||||
if (!wasOnline)
|
||||
{
|
||||
NotifyUserOnline(connection, sendNotification);
|
||||
}
|
||||
|
||||
if (registration.CharacterIdent is not null &&
|
||||
_pendingCharacterData.TryRemove(dto.User.UID, out var pendingData))
|
||||
{
|
||||
var pendingRegistration = new PairRegistration(new PairUniqueIdentifier(dto.User.UID), registration.CharacterIdent);
|
||||
var pendingApply = _handlerRegistry.ApplyCharacterData(pendingRegistration, pendingData);
|
||||
if (!pendingApply.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Applying pending character data for {Uid} failed: {Error}", dto.User.UID, pendingApply.Error);
|
||||
}
|
||||
}
|
||||
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
|
||||
public void HandleUserOffline(UserData user)
|
||||
{
|
||||
var registrationResult = _pairManager.MarkOffline(user);
|
||||
if (registrationResult.Success)
|
||||
{
|
||||
_pendingCharacterData.TryRemove(user.UID, out _);
|
||||
if (registrationResult.Value.CharacterIdent is not null)
|
||||
{
|
||||
_ = _handlerRegistry.DeregisterOfflinePair(registrationResult.Value);
|
||||
}
|
||||
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(user));
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
else if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("MarkOffline failed for {Uid}: {Error}", user.UID, registrationResult.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleUserPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
var pairResult = _pairManager.GetPair(dto.User.UID);
|
||||
if (!pairResult.Success)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Permission update received for unknown pair {Uid}", dto.User.UID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = pairResult.Value;
|
||||
var previous = connection.OtherToSelfPermissions;
|
||||
|
||||
var updateResult = _pairManager.UpdateOtherPermissions(dto);
|
||||
if (!updateResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update permissions for {Uid}: {Error}", dto.User.UID, updateResult.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged();
|
||||
|
||||
if (previous.IsPaused() != dto.Permissions.IsPaused())
|
||||
{
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||
|
||||
if (connection.Ident is not null)
|
||||
{
|
||||
var pauseResult = _handlerRegistry.SetPausedState(new PairUniqueIdentifier(dto.User.UID), connection.Ident, dto.Permissions.IsPaused());
|
||||
if (!pauseResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update pause state for {Uid}: {Error}", dto.User.UID, pauseResult.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!connection.IsPaused && connection.Ident is not null)
|
||||
{
|
||||
ReapplyLastKnownData(dto.User.UID, connection.Ident);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleSelfPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
var pairResult = _pairManager.GetPair(dto.User.UID);
|
||||
if (!pairResult.Success)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Self permission update received for unknown pair {Uid}", dto.User.UID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = pairResult.Value;
|
||||
var previous = connection.SelfToOtherPermissions;
|
||||
|
||||
var updateResult = _pairManager.UpdateSelfPermissions(dto);
|
||||
if (!updateResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update self permissions for {Uid}: {Error}", dto.User.UID, updateResult.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
PublishPairDataChanged();
|
||||
|
||||
if (previous.IsPaused() != dto.Permissions.IsPaused())
|
||||
{
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||
|
||||
if (connection.Ident is not null)
|
||||
{
|
||||
var pauseResult = _handlerRegistry.SetPausedState(new PairUniqueIdentifier(dto.User.UID), connection.Ident, dto.Permissions.IsPaused());
|
||||
if (!pauseResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to update pause state for {Uid}: {Error}", dto.User.UID, pauseResult.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!connection.IsPaused && connection.Ident is not null)
|
||||
{
|
||||
ReapplyLastKnownData(dto.User.UID, connection.Ident);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleUploadStatus(UserDto dto)
|
||||
{
|
||||
var pairResult = _pairManager.GetPair(dto.User.UID);
|
||||
if (!pairResult.Success)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Upload status received for unknown pair {Uid}", dto.User.UID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = pairResult.Value;
|
||||
if (connection.Ident is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var setResult = _handlerRegistry.SetUploading(new PairUniqueIdentifier(dto.User.UID), connection.Ident, true);
|
||||
if (!setResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to set uploading for {Uid}: {Error}", dto.User.UID, setResult.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleCharacterData(OnlineUserCharaDataDto dto)
|
||||
{
|
||||
var pairResult = _pairManager.GetPair(dto.User.UID);
|
||||
if (!pairResult.Success)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Character data received for unknown pair {Uid}, queued for later.", dto.User.UID);
|
||||
}
|
||||
_pendingCharacterData[dto.User.UID] = dto;
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = pairResult.Value;
|
||||
_mediator.Publish(new EventMessage(new Event(connection.User, nameof(PairCoordinator), EventSeverity.Informational, "Received Character Data")));
|
||||
if (connection.Ident is null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Character data received for {Uid} without ident, queued for later.", dto.User.UID);
|
||||
}
|
||||
_pendingCharacterData[dto.User.UID] = dto;
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingCharacterData.TryRemove(dto.User.UID, out _);
|
||||
var registration = new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident);
|
||||
var applyResult = _handlerRegistry.ApplyCharacterData(registration, dto);
|
||||
if (!applyResult.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("ApplyCharacterData queued for {Uid}: {Error}", dto.User.UID, applyResult.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleProfile(UserDto dto)
|
||||
{
|
||||
_mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// wires mediator events into the pair system
|
||||
/// </summary>
|
||||
public sealed partial class PairCoordinator : MediatorSubscriberBase
|
||||
{
|
||||
private readonly ILogger<PairCoordinator> _logger;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly PairHandlerRegistry _handlerRegistry;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly PairPerformanceMetricsCache _metricsCache;
|
||||
private readonly ConcurrentDictionary<string, OnlineUserCharaDataDto> _pendingCharacterData = new(StringComparer.Ordinal);
|
||||
|
||||
public PairCoordinator(
|
||||
ILogger<PairCoordinator> logger,
|
||||
LightlessConfigService configService,
|
||||
LightlessMediator mediator,
|
||||
PairHandlerRegistry handlerRegistry,
|
||||
PairManager pairManager,
|
||||
PairLedger pairLedger,
|
||||
ServerConfigurationManager serverConfigurationManager,
|
||||
PairPerformanceMetricsCache metricsCache)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_mediator = mediator;
|
||||
_handlerRegistry = handlerRegistry;
|
||||
_pairManager = pairManager;
|
||||
_pairLedger = pairLedger;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_metricsCache = metricsCache;
|
||||
|
||||
mediator.Subscribe<ActiveServerChangedMessage>(this, msg => HandleActiveServerChange(msg.ServerUrl));
|
||||
mediator.Subscribe<DisconnectedMessage>(this, _ => HandleDisconnected());
|
||||
}
|
||||
|
||||
internal PairLedger Ledger => _pairLedger;
|
||||
|
||||
private void PublishPairDataChanged(bool groupChanged = false)
|
||||
{
|
||||
_mediator.Publish(new RefreshUiMessage());
|
||||
_mediator.Publish(new PairDataChangedMessage());
|
||||
if (groupChanged)
|
||||
{
|
||||
_mediator.Publish(new GroupCollectionChangedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyUserOnline(PairConnection? connection, bool sendNotification)
|
||||
{
|
||||
if (connection is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var config = _configService.Current;
|
||||
if (config.ShowOnlineNotifications && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Pair {Uid} marked online", connection.User.UID);
|
||||
}
|
||||
|
||||
if (!sendNotification || !config.ShowOnlineNotifications)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.ShowOnlineNotificationsOnlyForIndividualPairs &&
|
||||
(!connection.IsDirectlyPaired || connection.IsOneSided))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var note = _serverConfigurationManager.GetNoteForUid(connection.User.UID);
|
||||
if (config.ShowOnlineNotificationsOnlyForNamedPairs &&
|
||||
string.IsNullOrEmpty(note))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = !string.IsNullOrEmpty(note)
|
||||
? $"{note} ({connection.User.AliasOrUID}) is now online"
|
||||
: $"{connection.User.AliasOrUID} is now online";
|
||||
|
||||
_mediator.Publish(new NotificationMessage("User online", message, NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
private void ReapplyLastKnownData(string userId, string ident, bool forced = false)
|
||||
{
|
||||
var result = _handlerRegistry.ApplyLastReceivedData(new PairUniqueIdentifier(userId), ident, forced);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to reapply cached data for {Uid}: {Error}", userId, result.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleActiveServerChange(string serverUrl)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Active server changed to {Server}", serverUrl);
|
||||
}
|
||||
|
||||
ResetPairState();
|
||||
}
|
||||
|
||||
private void HandleDisconnected()
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Lightless disconnected, clearing pair state");
|
||||
}
|
||||
|
||||
ResetPairState();
|
||||
}
|
||||
|
||||
private void ResetPairState()
|
||||
{
|
||||
_handlerRegistry.ResetAllHandlers();
|
||||
_pairManager.ClearAll();
|
||||
_pendingCharacterData.Clear();
|
||||
_metricsCache.ClearAll();
|
||||
_mediator.Publish(new ClearProfileUserDataMessage());
|
||||
_mediator.Publish(new ClearProfileGroupDataMessage());
|
||||
PublishPairDataChanged(groupChanged: true);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
public sealed record PairDebugInfo(
|
||||
bool HasHandler,
|
||||
bool HandlerInitialized,
|
||||
bool HandlerVisible,
|
||||
bool HandlerScheduledForDeletion,
|
||||
DateTime? LastDataReceivedAt,
|
||||
DateTime? LastApplyAttemptAt,
|
||||
DateTime? LastSuccessfulApplyAt,
|
||||
string? LastFailureReason,
|
||||
IReadOnlyList<string> BlockingConditions,
|
||||
bool IsApplying,
|
||||
bool IsDownloading,
|
||||
int PendingDownloadCount,
|
||||
int ForbiddenDownloadCount)
|
||||
{
|
||||
public static PairDebugInfo Empty { get; } = new(
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Array.Empty<string>(),
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.PairProcessing;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.Services.TextureCompression;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
{
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly FileDownloadManagerFactory _fileDownloadManagerFactory;
|
||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IHostApplicationLifetime _lifetime;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly TextureDownscaleService _textureDownscaleService;
|
||||
private readonly PairStateCache _pairStateCache;
|
||||
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
||||
|
||||
public PairHandlerAdapterFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
LightlessMediator mediator,
|
||||
PairManager pairManager,
|
||||
GameObjectHandlerFactory gameObjectHandlerFactory,
|
||||
IpcManager ipcManager,
|
||||
FileDownloadManagerFactory fileDownloadManagerFactory,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||
IServiceProvider serviceProvider,
|
||||
IHostApplicationLifetime lifetime,
|
||||
FileCacheManager fileCacheManager,
|
||||
PlayerPerformanceService playerPerformanceService,
|
||||
PairProcessingLimiter pairProcessingLimiter,
|
||||
ServerConfigurationManager serverConfigManager,
|
||||
TextureDownscaleService textureDownscaleService,
|
||||
PairStateCache pairStateCache,
|
||||
PairPerformanceMetricsCache pairPerformanceMetricsCache)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_mediator = mediator;
|
||||
_pairManager = pairManager;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
_ipcManager = ipcManager;
|
||||
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||
_serviceProvider = serviceProvider;
|
||||
_lifetime = lifetime;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
_textureDownscaleService = textureDownscaleService;
|
||||
_pairStateCache = pairStateCache;
|
||||
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
||||
}
|
||||
|
||||
public IPairHandlerAdapter Create(string ident)
|
||||
{
|
||||
var downloadManager = _fileDownloadManagerFactory.Create();
|
||||
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
||||
return new PairHandlerAdapter(
|
||||
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
|
||||
_mediator,
|
||||
_pairManager,
|
||||
ident,
|
||||
_gameObjectHandlerFactory,
|
||||
_ipcManager,
|
||||
downloadManager,
|
||||
_pluginWarningNotificationManager,
|
||||
dalamudUtilService,
|
||||
_lifetime,
|
||||
_fileCacheManager,
|
||||
_playerPerformanceService,
|
||||
_pairProcessingLimiter,
|
||||
_serverConfigManager,
|
||||
_textureDownscaleService,
|
||||
_pairStateCache,
|
||||
_pairPerformanceMetricsCache);
|
||||
}
|
||||
}
|
||||
@@ -1,486 +0,0 @@
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// creates, tracks, and removes pair handlers
|
||||
/// </summary>
|
||||
public sealed class PairHandlerRegistry : IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly object _pendingGate = new();
|
||||
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
|
||||
|
||||
private readonly IPairHandlerAdapterFactory _handlerFactory;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairStateCache _pairStateCache;
|
||||
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
||||
private readonly ILogger<PairHandlerRegistry> _logger;
|
||||
|
||||
private readonly TimeSpan _deletionGracePeriod = TimeSpan.FromMinutes(5);
|
||||
private static readonly TimeSpan _handlerReadyTimeout = TimeSpan.FromMinutes(3);
|
||||
private const int _handlerReadyPollDelayMs = 500;
|
||||
private readonly Dictionary<string, CancellationTokenSource> _pendingCharacterData = new(StringComparer.Ordinal);
|
||||
|
||||
public PairHandlerRegistry(
|
||||
IPairHandlerAdapterFactory handlerFactory,
|
||||
PairManager pairManager,
|
||||
PairStateCache pairStateCache,
|
||||
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
||||
ILogger<PairHandlerRegistry> logger)
|
||||
{
|
||||
_handlerFactory = handlerFactory;
|
||||
_pairManager = pairManager;
|
||||
_pairStateCache = pairStateCache;
|
||||
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public int GetVisibleUsersCount()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _entriesByHandler.Keys.Count(handler => handler.IsVisible);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsIdentVisible(string ident)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _entriesByIdent.TryGetValue(ident, out var entry) && entry.Handler.IsVisible;
|
||||
}
|
||||
}
|
||||
|
||||
public PairOperationResult<PairUniqueIdentifier> RegisterOnlinePair(PairRegistration registration)
|
||||
{
|
||||
if (registration.CharacterIdent is null)
|
||||
{
|
||||
return PairOperationResult<PairUniqueIdentifier>.Fail($"Registration for {registration.PairIdent.UserId} missing ident.");
|
||||
}
|
||||
|
||||
IPairHandlerAdapter handler;
|
||||
lock (_gate)
|
||||
{
|
||||
var entry = GetOrCreateEntry(registration.CharacterIdent);
|
||||
handler = entry.Handler;
|
||||
handler.ScheduledForDeletion = false;
|
||||
entry.AddPair(registration.PairIdent);
|
||||
if (!handler.Initialized)
|
||||
{
|
||||
handler.Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
ApplyPauseStateForHandler(handler);
|
||||
|
||||
if (handler.LastReceivedCharacterData is null)
|
||||
{
|
||||
var cachedData = _pairStateCache.TryLoad(registration.CharacterIdent);
|
||||
if (cachedData is not null)
|
||||
{
|
||||
handler.LoadCachedCharacterData(cachedData);
|
||||
}
|
||||
}
|
||||
|
||||
if (handler.LastReceivedCharacterData is not null &&
|
||||
(handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0))
|
||||
{
|
||||
handler.ApplyLastReceivedData(forced: true);
|
||||
}
|
||||
|
||||
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
||||
}
|
||||
|
||||
public PairOperationResult<PairUniqueIdentifier> DeregisterOfflinePair(PairRegistration registration, bool forceDisposal = false)
|
||||
{
|
||||
if (registration.CharacterIdent is null)
|
||||
{
|
||||
return PairOperationResult<PairUniqueIdentifier>.Fail($"Deregister for {registration.PairIdent.UserId} missing ident.");
|
||||
}
|
||||
|
||||
IPairHandlerAdapter? handler = null;
|
||||
bool shouldScheduleRemoval = false;
|
||||
bool shouldDisposeImmediately = false;
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_entriesByIdent.TryGetValue(registration.CharacterIdent, out var entry))
|
||||
{
|
||||
return PairOperationResult<PairUniqueIdentifier>.Fail($"Ident {registration.CharacterIdent} not registered.");
|
||||
}
|
||||
|
||||
handler = entry.Handler;
|
||||
entry.RemovePair(registration.PairIdent);
|
||||
if (entry.PairCount == 0)
|
||||
{
|
||||
if (forceDisposal)
|
||||
{
|
||||
shouldDisposeImmediately = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldScheduleRemoval = true;
|
||||
handler.ScheduledForDeletion = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDisposeImmediately && handler is not null)
|
||||
{
|
||||
if (TryFinalizeHandlerRemoval(handler))
|
||||
{
|
||||
handler.Dispose();
|
||||
}
|
||||
}
|
||||
else if (shouldScheduleRemoval && handler is not null)
|
||||
{
|
||||
_ = RemoveAfterGracePeriodAsync(handler);
|
||||
}
|
||||
|
||||
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
||||
}
|
||||
|
||||
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
||||
{
|
||||
if (registration.CharacterIdent is null)
|
||||
{
|
||||
return PairOperationResult.Fail($"Character data received without ident for {registration.PairIdent.UserId}.");
|
||||
}
|
||||
|
||||
if (!TryGetHandler(registration.CharacterIdent, out var handler) || handler is null)
|
||||
{
|
||||
var registerResult = RegisterOnlinePair(registration);
|
||||
if (!registerResult.Success)
|
||||
{
|
||||
return PairOperationResult.Fail(registerResult.Error);
|
||||
}
|
||||
|
||||
if (!TryGetHandler(registration.CharacterIdent, out handler) || handler is null)
|
||||
{
|
||||
QueuePendingCharacterData(registration, dto);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
if (!handler.Initialized)
|
||||
{
|
||||
handler.Initialize();
|
||||
QueuePendingCharacterData(registration, dto);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
|
||||
handler.ApplyData(dto.CharaData);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
|
||||
public PairOperationResult ApplyLastReceivedData(PairUniqueIdentifier pairIdent, string ident, bool forced = false)
|
||||
{
|
||||
if (!TryGetHandler(ident, out var handler) || handler is null)
|
||||
{
|
||||
return PairOperationResult.Fail($"Cannot reapply data: handler for {pairIdent.UserId} not found.");
|
||||
}
|
||||
|
||||
handler.ApplyLastReceivedData(forced);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
|
||||
public PairOperationResult SetUploading(PairUniqueIdentifier pairIdent, string ident, bool uploading)
|
||||
{
|
||||
if (!TryGetHandler(ident, out var handler) || handler is null)
|
||||
{
|
||||
return PairOperationResult.Fail($"Cannot set uploading for {pairIdent.UserId}: handler not found.");
|
||||
}
|
||||
|
||||
handler.SetUploading(uploading);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
|
||||
public PairOperationResult SetPausedState(PairUniqueIdentifier pairIdent, string ident, bool paused)
|
||||
{
|
||||
if (!TryGetHandler(ident, out var handler) || handler is null)
|
||||
{
|
||||
return PairOperationResult.Fail($"Cannot update pause state for {pairIdent.UserId}: handler not found.");
|
||||
}
|
||||
|
||||
_ = paused; // value reflected in pair manager already
|
||||
ApplyPauseStateForHandler(handler);
|
||||
return PairOperationResult.Ok();
|
||||
}
|
||||
|
||||
public PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>> GetPairConnections(string ident)
|
||||
{
|
||||
PairHandlerEntry? entry;
|
||||
lock (_gate)
|
||||
{
|
||||
_entriesByIdent.TryGetValue(ident, out entry);
|
||||
}
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
return PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>>.Fail($"No handler registered for {ident}.");
|
||||
}
|
||||
|
||||
var list = new List<(PairUniqueIdentifier, PairConnection)>();
|
||||
foreach (var pairIdent in entry.SnapshotPairs())
|
||||
{
|
||||
var result = _pairManager.GetPair(pairIdent.UserId);
|
||||
if (result.Success)
|
||||
{
|
||||
list.Add((pairIdent, result.Value));
|
||||
}
|
||||
}
|
||||
|
||||
return PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>>.Ok(list);
|
||||
}
|
||||
|
||||
private void ApplyPauseStateForHandler(IPairHandlerAdapter handler)
|
||||
{
|
||||
var pairs = _pairManager.GetPairsByIdent(handler.Ident);
|
||||
bool paused = pairs.Any(p => p.SelfToOtherPermissions.IsPaused() || p.OtherToSelfPermissions.IsPaused());
|
||||
handler.SetPaused(paused);
|
||||
}
|
||||
|
||||
internal bool TryGetHandler(string ident, out IPairHandlerAdapter? handler)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var success = _entriesByIdent.TryGetValue(ident, out var entry);
|
||||
handler = entry?.Handler;
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
internal IReadOnlyList<IPairHandlerAdapter> GetHandlerSnapshot()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _entriesByHandler.Keys.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
internal IReadOnlyCollection<PairUniqueIdentifier> GetRegisteredPairs(IPairHandlerAdapter handler)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_entriesByHandler.TryGetValue(handler, out var entry))
|
||||
{
|
||||
return entry.SnapshotPairs();
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<PairUniqueIdentifier>();
|
||||
}
|
||||
|
||||
internal void ReapplyAll(bool forced = false)
|
||||
{
|
||||
var handlers = GetHandlerSnapshot();
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
handler.ApplyLastReceivedData(forced);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to reapply cached data for {Ident}", handler.Ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void ResetAllHandlers()
|
||||
{
|
||||
List<IPairHandlerAdapter> handlers;
|
||||
lock (_gate)
|
||||
{
|
||||
handlers = _entriesByHandler.Keys.ToList();
|
||||
_entriesByIdent.Clear();
|
||||
_entriesByHandler.Clear();
|
||||
}
|
||||
|
||||
CancelAllPendingCharacterData();
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
handler.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to dispose handler for {Ident}", handler.Ident);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
List<IPairHandlerAdapter> handlers;
|
||||
lock (_gate)
|
||||
{
|
||||
handlers = _entriesByHandler.Keys.ToList();
|
||||
_entriesByIdent.Clear();
|
||||
_entriesByHandler.Clear();
|
||||
}
|
||||
|
||||
CancelAllPendingCharacterData();
|
||||
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
handler.Dispose();
|
||||
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
||||
}
|
||||
}
|
||||
|
||||
private PairHandlerEntry GetOrCreateEntry(string ident)
|
||||
{
|
||||
if (_entriesByIdent.TryGetValue(ident, out var entry))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
var handler = _handlerFactory.Create(ident);
|
||||
entry = new PairHandlerEntry(ident, handler);
|
||||
_entriesByIdent[ident] = entry;
|
||||
_entriesByHandler[handler] = entry;
|
||||
return entry;
|
||||
}
|
||||
|
||||
private async Task RemoveAfterGracePeriodAsync(IPairHandlerAdapter handler)
|
||||
{
|
||||
await Task.Delay(_deletionGracePeriod).ConfigureAwait(false);
|
||||
|
||||
if (TryFinalizeHandlerRemoval(handler))
|
||||
{
|
||||
handler.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryFinalizeHandlerRemoval(IPairHandlerAdapter handler)
|
||||
{
|
||||
string? ident = null;
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_entriesByHandler.TryGetValue(handler, out var entry) || entry.HasPairs)
|
||||
{
|
||||
handler.ScheduledForDeletion = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
ident = entry.Ident;
|
||||
_entriesByHandler.Remove(handler);
|
||||
_entriesByIdent.Remove(entry.Ident);
|
||||
}
|
||||
|
||||
if (ident is not null)
|
||||
{
|
||||
_pairPerformanceMetricsCache.Clear(ident);
|
||||
CancelPendingCharacterData(ident);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void QueuePendingCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
||||
{
|
||||
if (registration.CharacterIdent is null) return;
|
||||
|
||||
CancellationTokenSource? previous;
|
||||
CancellationTokenSource cts;
|
||||
|
||||
lock (_pendingGate)
|
||||
{
|
||||
_pendingCharacterData.TryGetValue(registration.CharacterIdent, out previous);
|
||||
previous?.Cancel();
|
||||
|
||||
cts = new CancellationTokenSource();
|
||||
_pendingCharacterData[registration.CharacterIdent] = cts;
|
||||
}
|
||||
|
||||
cts.CancelAfter(_handlerReadyTimeout);
|
||||
_ = Task.Run(() => WaitThenApplyPendingCharacterDataAsync(registration, dto, cts.Token, cts));
|
||||
}
|
||||
|
||||
private void CancelPendingCharacterData(string ident)
|
||||
{
|
||||
CancellationTokenSource? cts = null;
|
||||
lock (_pendingGate)
|
||||
{
|
||||
if (_pendingCharacterData.TryGetValue(ident, out cts))
|
||||
_pendingCharacterData.Remove(ident);
|
||||
}
|
||||
|
||||
cts?.Cancel();
|
||||
}
|
||||
|
||||
private void CancelAllPendingCharacterData()
|
||||
{
|
||||
List<CancellationTokenSource>? snapshot = null;
|
||||
lock (_pendingGate)
|
||||
{
|
||||
if (_pendingCharacterData.Count > 0)
|
||||
{
|
||||
snapshot = [.. _pendingCharacterData.Values];
|
||||
_pendingCharacterData.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshot is null) return;
|
||||
foreach (var cts in snapshot) cts.Cancel();
|
||||
}
|
||||
|
||||
private async Task WaitThenApplyPendingCharacterDataAsync(
|
||||
PairRegistration registration,
|
||||
OnlineUserCharaDataDto dto,
|
||||
CancellationToken token,
|
||||
CancellationTokenSource source)
|
||||
{
|
||||
if (registration.CharacterIdent is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
if (TryGetHandler(registration.CharacterIdent, out var handler) && handler is not null && handler.Initialized)
|
||||
{
|
||||
handler.ApplyData(dto.CharaData);
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(_handlerReadyPollDelayMs, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// expected
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_pendingGate)
|
||||
{
|
||||
if (_pendingCharacterData.TryGetValue(registration.CharacterIdent, out var current) && ReferenceEquals(current, source))
|
||||
{
|
||||
_pendingCharacterData.Remove(registration.CharacterIdent);
|
||||
}
|
||||
}
|
||||
|
||||
source.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// keeps pair info for ui and reapplication
|
||||
/// </summary>
|
||||
public sealed class PairLedger : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairHandlerRegistry _registry;
|
||||
private readonly ILogger<PairLedger> _logger;
|
||||
private readonly object _metricsGate = new();
|
||||
private CancellationTokenSource? _ensureMetricsCts;
|
||||
|
||||
public PairLedger(
|
||||
ILogger<PairLedger> logger,
|
||||
LightlessMediator mediator,
|
||||
PairManager pairManager,
|
||||
PairHandlerRegistry registry) : base(logger, mediator)
|
||||
{
|
||||
_pairManager = pairManager;
|
||||
_registry = registry;
|
||||
_logger = logger;
|
||||
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, _ => ReapplyAll(forced: true));
|
||||
Mediator.Subscribe<GposeEndMessage>(this, _ => ReapplyAll());
|
||||
Mediator.Subscribe<PenumbraInitializedMessage>(this, _ => ReapplyAll(forced: true));
|
||||
Mediator.Subscribe<FileCacheInitializedMessage>(this, _ => ReapplyAll(forced: true));
|
||||
Mediator.Subscribe<DisconnectedMessage>(this, _ => Reset());
|
||||
Mediator.Subscribe<ConnectedMessage>(this, _ => ScheduleEnsureMetrics(TimeSpan.FromSeconds(2)));
|
||||
Mediator.Subscribe<HubReconnectedMessage>(this, _ => ScheduleEnsureMetrics(TimeSpan.FromSeconds(2)));
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, _ => ScheduleEnsureMetrics(TimeSpan.FromSeconds(2)));
|
||||
Mediator.Subscribe<VisibilityChange>(this, _ => EnsureMetricsForVisiblePairs());
|
||||
}
|
||||
|
||||
public bool IsPairVisible(PairUniqueIdentifier pairIdent)
|
||||
{
|
||||
var connectionResult = _pairManager.GetPair(pairIdent.UserId);
|
||||
if (!connectionResult.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var connection = connectionResult.Value;
|
||||
if (connection.Ident is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _registry.IsIdentVisible(connection.Ident);
|
||||
}
|
||||
|
||||
public IPairHandlerAdapter? GetHandler(PairUniqueIdentifier pairIdent)
|
||||
{
|
||||
var connectionResult = _pairManager.GetPair(pairIdent.UserId);
|
||||
if (!connectionResult.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var connection = connectionResult.Value;
|
||||
if (connection.Ident is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _registry.TryGetHandler(connection.Ident, out var handler) ? handler : null;
|
||||
}
|
||||
|
||||
public IReadOnlyList<PairConnection> GetVisiblePairs()
|
||||
{
|
||||
return _pairManager.GetAllPairs()
|
||||
.Select(kv => kv.Value)
|
||||
.Where(connection => connection.Ident is not null && _registry.IsIdentVisible(connection.Ident))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public IReadOnlyList<GroupFullInfoDto> GetAllGroupInfos()
|
||||
{
|
||||
return _pairManager.GetAllGroups()
|
||||
.Select(kv => kv.Value.GroupFullInfo)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, Syncshell> GetAllSyncshells()
|
||||
{
|
||||
return _pairManager.GetAllGroups();
|
||||
}
|
||||
|
||||
public void ReapplyAll(bool forced = false)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
_logger.LogTrace("Reapplying cached data for all handlers (forced: {Forced})", forced);
|
||||
}
|
||||
|
||||
_registry.ReapplyAll(forced);
|
||||
}
|
||||
|
||||
public void ReapplyPair(PairUniqueIdentifier pairIdent, bool forced = false)
|
||||
{
|
||||
var connectionResult = _pairManager.GetPair(pairIdent.UserId);
|
||||
if (!connectionResult.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = connectionResult.Value;
|
||||
if (connection.Ident is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = _registry.ApplyLastReceivedData(pairIdent, connection.Ident, forced);
|
||||
if (!result.Success && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to reapply data for {UserId}: {Error}", pairIdent.UserId, result.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
_logger.LogTrace("Resetting pair handlers after disconnect.");
|
||||
}
|
||||
|
||||
CancelScheduledMetrics();
|
||||
}
|
||||
|
||||
public IReadOnlyList<PairDisplayEntry> GetAllEntries()
|
||||
{
|
||||
var groups = _pairManager.GetAllGroups();
|
||||
var list = new List<PairDisplayEntry>();
|
||||
foreach (var (userId, connection) in _pairManager.GetAllPairs())
|
||||
{
|
||||
var ident = new PairUniqueIdentifier(userId);
|
||||
IPairHandlerAdapter? handler = null;
|
||||
if (connection.Ident is not null)
|
||||
{
|
||||
_registry.TryGetHandler(connection.Ident, out handler);
|
||||
}
|
||||
|
||||
var groupInfos = connection.Groups.Keys
|
||||
.Select(gid =>
|
||||
{
|
||||
if (groups.TryGetValue(gid, out var shell))
|
||||
{
|
||||
return shell.GroupFullInfo;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.Where(dto => dto is not null)
|
||||
.Cast<GroupFullInfoDto>()
|
||||
.ToList();
|
||||
|
||||
list.Add(new PairDisplayEntry(ident, connection, groupInfos, handler));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public bool TryGetEntry(PairUniqueIdentifier ident, out PairDisplayEntry? entry)
|
||||
{
|
||||
entry = null;
|
||||
var connectionResult = _pairManager.GetPair(ident.UserId);
|
||||
if (!connectionResult.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var connection = connectionResult.Value;
|
||||
var groups = connection.Groups.Keys
|
||||
.Select(gid =>
|
||||
{
|
||||
var groupResult = _pairManager.GetGroup(gid);
|
||||
return groupResult.Success ? groupResult.Value.GroupFullInfo : null;
|
||||
})
|
||||
.Where(dto => dto is not null)
|
||||
.Cast<GroupFullInfoDto>()
|
||||
.ToList();
|
||||
|
||||
IPairHandlerAdapter? handler = null;
|
||||
if (connection.Ident is not null)
|
||||
{
|
||||
_registry.TryGetHandler(connection.Ident, out handler);
|
||||
}
|
||||
|
||||
entry = new PairDisplayEntry(ident, connection, groups, handler);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ScheduleEnsureMetrics(TimeSpan? delay = null)
|
||||
{
|
||||
lock (_metricsGate)
|
||||
{
|
||||
_ensureMetricsCts?.Cancel();
|
||||
var cts = new CancellationTokenSource();
|
||||
_ensureMetricsCts = cts;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (delay is { } d && d > TimeSpan.Zero)
|
||||
{
|
||||
await Task.Delay(d, cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
EnsureMetricsForVisiblePairs();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_metricsGate)
|
||||
{
|
||||
if (_ensureMetricsCts == cts)
|
||||
{
|
||||
_ensureMetricsCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
cts.Dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelScheduledMetrics()
|
||||
{
|
||||
lock (_metricsGate)
|
||||
{
|
||||
_ensureMetricsCts?.Cancel();
|
||||
_ensureMetricsCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureMetricsForVisiblePairs()
|
||||
{
|
||||
var handlers = _registry.GetHandlerSnapshot();
|
||||
foreach (var handler in handlers)
|
||||
{
|
||||
if (!handler.IsVisible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handler.LastReceivedCharacterData is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handler.LastAppliedApproximateVRAMBytes >= 0
|
||||
&& handler.LastAppliedDataTris >= 0
|
||||
&& handler.LastAppliedApproximateEffectiveVRAMBytes >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handler.FetchPerformanceMetricsFromCache())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
handler.ApplyLastReceivedData(forced: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to ensure performance metrics for {Ident}", handler.Ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
CancelScheduledMetrics();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,220 +0,0 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// core models for the pair system
|
||||
/// </summary>
|
||||
public sealed class PairState
|
||||
{
|
||||
public CharacterData? CharacterData { get; set; }
|
||||
public Guid? TemporaryCollectionId { get; set; }
|
||||
|
||||
public bool IsEmpty => CharacterData is null && (TemporaryCollectionId is null || TemporaryCollectionId == Guid.Empty);
|
||||
}
|
||||
|
||||
public readonly record struct PairUniqueIdentifier(string UserId);
|
||||
|
||||
/// <summary>
|
||||
/// link between a pair id and character ident
|
||||
/// </summary>
|
||||
public sealed record PairRegistration(PairUniqueIdentifier PairIdent, string? CharacterIdent);
|
||||
|
||||
/// <summary>
|
||||
/// per group membership info for a pair
|
||||
/// </summary>
|
||||
public sealed class GroupPairRelationship
|
||||
{
|
||||
public GroupPairRelationship(string groupId, GroupPairUserInfo? info)
|
||||
{
|
||||
GroupId = groupId;
|
||||
UserInfo = info;
|
||||
}
|
||||
|
||||
public string GroupId { get; }
|
||||
public GroupPairUserInfo? UserInfo { get; private set; }
|
||||
|
||||
public void SetUserInfo(GroupPairUserInfo? info)
|
||||
{
|
||||
UserInfo = info;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// runtime view of a single pair connection
|
||||
/// </summary>
|
||||
public sealed class PairConnection
|
||||
{
|
||||
public PairConnection(UserData user)
|
||||
{
|
||||
User = user;
|
||||
Groups = new Dictionary<string, GroupPairRelationship>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public UserData User { get; }
|
||||
public bool IsOnline { get; private set; }
|
||||
public string? Ident { get; private set; }
|
||||
public UserPermissions SelfToOtherPermissions { get; private set; } = UserPermissions.NoneSet;
|
||||
public UserPermissions OtherToSelfPermissions { get; private set; } = UserPermissions.NoneSet;
|
||||
public IndividualPairStatus? IndividualPairStatus { get; private set; }
|
||||
public Dictionary<string, GroupPairRelationship> Groups { get; }
|
||||
|
||||
public bool IsPaused => SelfToOtherPermissions.IsPaused();
|
||||
public bool IsDirectlyPaired => IndividualPairStatus is not null && IndividualPairStatus != API.Data.Enum.IndividualPairStatus.None;
|
||||
public bool IsOneSided => IndividualPairStatus == API.Data.Enum.IndividualPairStatus.OneSided;
|
||||
public bool HasAnyConnection => IsDirectlyPaired || Groups.Count > 0;
|
||||
|
||||
public void SetOnline(string? ident)
|
||||
{
|
||||
IsOnline = true;
|
||||
Ident = ident;
|
||||
}
|
||||
|
||||
public void SetOffline()
|
||||
{
|
||||
IsOnline = false;
|
||||
}
|
||||
|
||||
public void UpdatePermissions(UserPermissions own, UserPermissions other)
|
||||
{
|
||||
SelfToOtherPermissions = own;
|
||||
OtherToSelfPermissions = other;
|
||||
}
|
||||
|
||||
public void UpdateStatus(IndividualPairStatus? status)
|
||||
{
|
||||
IndividualPairStatus = status;
|
||||
}
|
||||
|
||||
public void EnsureGroupRelationship(string groupId, GroupPairUserInfo? info)
|
||||
{
|
||||
if (Groups.TryGetValue(groupId, out var relationship))
|
||||
{
|
||||
relationship.SetUserInfo(info);
|
||||
}
|
||||
else
|
||||
{
|
||||
Groups[groupId] = new GroupPairRelationship(groupId, info);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveGroupRelationship(string groupId)
|
||||
{
|
||||
Groups.Remove(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// syncshell metadata plus member connections
|
||||
/// </summary>
|
||||
public sealed class Syncshell
|
||||
{
|
||||
public Syncshell(GroupFullInfoDto dto)
|
||||
{
|
||||
GroupFullInfo = dto;
|
||||
Users = new Dictionary<string, PairConnection>(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public GroupFullInfoDto GroupFullInfo { get; private set; }
|
||||
public Dictionary<string, PairConnection> Users { get; }
|
||||
|
||||
public void Update(GroupFullInfoDto dto)
|
||||
{
|
||||
GroupFullInfo = dto;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// simple success/failure result
|
||||
/// </summary>
|
||||
public readonly struct PairOperationResult
|
||||
{
|
||||
private PairOperationResult(bool success, string? error)
|
||||
{
|
||||
Success = success;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public bool Success { get; }
|
||||
public string? Error { get; }
|
||||
|
||||
public static PairOperationResult Ok() => new(true, null);
|
||||
|
||||
public static PairOperationResult Fail(string error) => new(false, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// typed success/failure result
|
||||
/// </summary>
|
||||
public readonly struct PairOperationResult<T>
|
||||
{
|
||||
private PairOperationResult(bool success, T value, string? error)
|
||||
{
|
||||
Success = success;
|
||||
Value = value;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public bool Success { get; }
|
||||
public T Value { get; }
|
||||
public string? Error { get; }
|
||||
|
||||
public static PairOperationResult<T> Ok(T value) => new(true, value, null);
|
||||
|
||||
public static PairOperationResult<T> Fail(string error) => new(false, default!, error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// state of which optional plugin warnings were shown
|
||||
/// </summary>
|
||||
public record OptionalPluginWarning
|
||||
{
|
||||
public bool ShownHeelsWarning { get; set; } = false;
|
||||
public bool ShownCustomizePlusWarning { get; set; } = false;
|
||||
public bool ShownHonorificWarning { get; set; } = false;
|
||||
public bool ShownMoodlesWarning { get; set; } = false;
|
||||
public bool ShowPetNicknamesWarning { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// tracks the handler registered pairs for an ident
|
||||
/// </summary>
|
||||
internal sealed class PairHandlerEntry
|
||||
{
|
||||
private readonly HashSet<PairUniqueIdentifier> _pairs = new();
|
||||
|
||||
public PairHandlerEntry(string ident, IPairHandlerAdapter handler)
|
||||
{
|
||||
Ident = ident;
|
||||
Handler = handler;
|
||||
}
|
||||
|
||||
public string Ident { get; }
|
||||
public IPairHandlerAdapter Handler { get; }
|
||||
|
||||
public bool HasPairs => _pairs.Count > 0;
|
||||
public int PairCount => _pairs.Count;
|
||||
|
||||
public void AddPair(PairUniqueIdentifier pair)
|
||||
{
|
||||
_pairs.Add(pair);
|
||||
}
|
||||
|
||||
public bool RemovePair(PairUniqueIdentifier pair)
|
||||
{
|
||||
return _pairs.Remove(pair);
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<PairUniqueIdentifier> SnapshotPairs()
|
||||
{
|
||||
if (_pairs.Count == 0)
|
||||
{
|
||||
return Array.Empty<PairUniqueIdentifier>();
|
||||
}
|
||||
|
||||
return _pairs.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
public readonly record struct PairPerformanceMetrics(
|
||||
long TriangleCount,
|
||||
long ApproximateVramBytes,
|
||||
long ApproximateEffectiveVramBytes);
|
||||
|
||||
/// <summary>
|
||||
/// caches performance metrics keyed by pair ident
|
||||
/// </summary>
|
||||
public sealed class PairPerformanceMetricsCache
|
||||
{
|
||||
private sealed record CacheEntry(string DataHash, PairPerformanceMetrics Metrics);
|
||||
|
||||
private readonly ConcurrentDictionary<string, CacheEntry> _cache = new(StringComparer.Ordinal);
|
||||
|
||||
public bool TryGetMetrics(string ident, string dataHash, out PairPerformanceMetrics metrics)
|
||||
{
|
||||
metrics = default;
|
||||
if (string.IsNullOrEmpty(ident) || string.IsNullOrEmpty(dataHash))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_cache.TryGetValue(ident, out var entry))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(entry.DataHash, dataHash, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
metrics = entry.Metrics;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void StoreMetrics(string ident, string dataHash, PairPerformanceMetrics metrics)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ident) || string.IsNullOrEmpty(dataHash))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cache[ident] = new CacheEntry(dataHash, metrics);
|
||||
}
|
||||
|
||||
public void Clear(string ident)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ident))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cache.TryRemove(ident, out _);
|
||||
}
|
||||
|
||||
public void ClearAll()
|
||||
{
|
||||
_cache.Clear();
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.Utils;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// cache for character/pair data and penumbra collections
|
||||
/// </summary>
|
||||
public sealed class PairStateCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, PairState> _cache = new(StringComparer.Ordinal);
|
||||
|
||||
public void Store(string ident, CharacterData data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ident) || data is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var state = _cache.GetOrAdd(ident, _ => new PairState());
|
||||
state.CharacterData = data.DeepClone();
|
||||
}
|
||||
|
||||
public CharacterData? TryLoad(string ident)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ident))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_cache.TryGetValue(ident, out var state) && state.CharacterData is not null)
|
||||
{
|
||||
return state.CharacterData.DeepClone();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Guid? TryGetTemporaryCollection(string ident)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ident))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_cache.TryGetValue(ident, out var state))
|
||||
{
|
||||
return state.TemporaryCollectionId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Guid? StoreTemporaryCollection(string ident, Guid collection)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ident) || collection == Guid.Empty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var state = _cache.GetOrAdd(ident, _ => new PairState());
|
||||
state.TemporaryCollectionId = collection;
|
||||
return collection;
|
||||
}
|
||||
|
||||
public Guid? ClearTemporaryCollection(string ident)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ident))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_cache.TryGetValue(ident, out var state))
|
||||
{
|
||||
var existing = state.TemporaryCollectionId;
|
||||
state.TemporaryCollectionId = null;
|
||||
TryRemoveIfEmpty(ident, state);
|
||||
return existing;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IReadOnlyList<Guid> ClearAllTemporaryCollections()
|
||||
{
|
||||
var removed = new List<Guid>();
|
||||
foreach (var (ident, state) in _cache)
|
||||
{
|
||||
if (state.TemporaryCollectionId is { } guid && guid != Guid.Empty)
|
||||
{
|
||||
removed.Add(guid);
|
||||
state.TemporaryCollectionId = null;
|
||||
}
|
||||
|
||||
TryRemoveIfEmpty(ident, state);
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
public void Clear(string ident)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ident))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cache.TryRemove(ident, out _);
|
||||
}
|
||||
|
||||
private void TryRemoveIfEmpty(string ident, PairState state)
|
||||
{
|
||||
if (state.IsEmpty)
|
||||
{
|
||||
_cache.TryRemove(ident, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Comparer;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
@@ -9,29 +8,27 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// pushes character data to visible pairs
|
||||
/// </summary>
|
||||
public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly ApiController _apiController;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly FileUploadManager _fileTransferManager;
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly PairManager _pairManager;
|
||||
private CharacterData? _lastCreatedData;
|
||||
private CharacterData? _uploadingCharacterData = null;
|
||||
private readonly List<UserData> _previouslyVisiblePlayers = [];
|
||||
private Task<CharacterData>? _fileUploadTask = null;
|
||||
private readonly HashSet<UserData> _usersToPushDataTo = new(UserDataComparer.Instance);
|
||||
private readonly SemaphoreSlim _pushLock = new(1, 1);
|
||||
private readonly HashSet<UserData> _usersToPushDataTo = [];
|
||||
private readonly SemaphoreSlim _pushDataSemaphore = new(1, 1);
|
||||
private readonly CancellationTokenSource _runtimeCts = new();
|
||||
|
||||
|
||||
public VisibleUserDataDistributor(ILogger<VisibleUserDataDistributor> logger, ApiController apiController, DalamudUtilService dalamudUtil,
|
||||
PairLedger pairLedger, LightlessMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator)
|
||||
PairManager pairManager, LightlessMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator)
|
||||
{
|
||||
_apiController = apiController;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pairLedger = pairLedger;
|
||||
_pairManager = pairManager;
|
||||
_fileTransferManager = fileTransferManager;
|
||||
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => FrameworkOnUpdate());
|
||||
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
|
||||
@@ -50,14 +47,7 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||
});
|
||||
|
||||
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
|
||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
|
||||
{
|
||||
_fileTransferManager.CancelUpload();
|
||||
_previouslyVisiblePlayers.Clear();
|
||||
_usersToPushDataTo.Clear();
|
||||
_uploadingCharacterData = null;
|
||||
_fileUploadTask = null;
|
||||
});
|
||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) => _previouslyVisiblePlayers.Clear());
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -73,7 +63,7 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||
|
||||
private void PushToAllVisibleUsers(bool forced = false)
|
||||
{
|
||||
foreach (var user in GetVisibleUsers())
|
||||
foreach (var user in _pairManager.GetVisibleUsers())
|
||||
{
|
||||
_usersToPushDataTo.Add(user);
|
||||
}
|
||||
@@ -89,8 +79,8 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
if (!_dalamudUtil.GetIsPlayerPresent() || !_apiController.IsConnected) return;
|
||||
|
||||
var allVisibleUsers = GetVisibleUsers();
|
||||
var newVisibleUsers = allVisibleUsers.Except(_previouslyVisiblePlayers, UserDataComparer.Instance).ToList();
|
||||
var allVisibleUsers = _pairManager.GetVisibleUsers();
|
||||
var newVisibleUsers = allVisibleUsers.Except(_previouslyVisiblePlayers).ToList();
|
||||
_previouslyVisiblePlayers.Clear();
|
||||
_previouslyVisiblePlayers.AddRange(allVisibleUsers);
|
||||
if (newVisibleUsers.Count == 0) return;
|
||||
@@ -108,49 +98,46 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||
private void PushCharacterData(bool forced = false)
|
||||
{
|
||||
if (_lastCreatedData == null || _usersToPushDataTo.Count == 0) return;
|
||||
_ = PushCharacterDataAsync(forced);
|
||||
}
|
||||
|
||||
private async Task PushCharacterDataAsync(bool forced = false)
|
||||
{
|
||||
await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
|
||||
try
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
if (_lastCreatedData == null || _usersToPushDataTo.Count == 0)
|
||||
return;
|
||||
try
|
||||
{
|
||||
forced |= _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash;
|
||||
|
||||
var hashChanged = _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash;
|
||||
forced |= hashChanged;
|
||||
|
||||
if (_fileUploadTask == null || _fileUploadTask.IsCompleted || forced)
|
||||
if (_fileUploadTask == null || (_fileUploadTask?.IsCompleted ?? false) || forced)
|
||||
{
|
||||
_uploadingCharacterData = _lastCreatedData.DeepClone();
|
||||
var uploadTargets = _usersToPushDataTo.ToList();
|
||||
Logger.LogDebug("Starting UploadTask for {hash}, Reason: TaskIsNull: {task}, TaskIsCompleted: {taskCpl}, Forced: {frc}",
|
||||
_lastCreatedData.DataHash,
|
||||
_fileUploadTask == null,
|
||||
_fileUploadTask?.IsCompleted ?? false,
|
||||
forced);
|
||||
|
||||
_fileUploadTask = _fileTransferManager.UploadFiles(_uploadingCharacterData, uploadTargets);
|
||||
_lastCreatedData.DataHash, _fileUploadTask == null, _fileUploadTask?.IsCompleted ?? false, forced);
|
||||
_fileUploadTask = _fileTransferManager.UploadFiles(_uploadingCharacterData, [.. _usersToPushDataTo]);
|
||||
}
|
||||
|
||||
var dataToSend = await _fileUploadTask.ConfigureAwait(false);
|
||||
|
||||
var users = _usersToPushDataTo.ToList();
|
||||
if (users.Count == 0)
|
||||
return;
|
||||
|
||||
Logger.LogDebug("Pushing {data} to {users}", dataToSend.DataHash, string.Join(", ", users.Select(k => k.AliasOrUID)));
|
||||
|
||||
await _apiController.PushCharacterData(dataToSend, users).ConfigureAwait(false);
|
||||
_usersToPushDataTo.Clear();
|
||||
if (_fileUploadTask != null)
|
||||
{
|
||||
var dataToSend = await _fileUploadTask.ConfigureAwait(false);
|
||||
await _pushDataSemaphore.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (_usersToPushDataTo.Count == 0) return;
|
||||
Logger.LogDebug("Pushing {data} to {users}", dataToSend.DataHash, string.Join(", ", _usersToPushDataTo.Select(k => k.AliasOrUID)));
|
||||
await _apiController.PushCharacterData(dataToSend, [.. _usersToPushDataTo]).ConfigureAwait(false);
|
||||
_usersToPushDataTo.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pushDataSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
catch (OperationCanceledException) when (_runtimeCts.IsCancellationRequested)
|
||||
{
|
||||
_pushLock.Release();
|
||||
Logger.LogDebug("PushCharacterData cancelled");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to push character data");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private List<UserData> GetVisibleUsers() => [.. _pairLedger.GetVisiblePairs().Select(connection => connection.User)];
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
|
||||
private readonly CancellationTokenSource _runtimeCts = new();
|
||||
private CancellationTokenSource _creationCts = new();
|
||||
private CancellationTokenSource _debounceCts = new();
|
||||
private string? _lastPublishedHash;
|
||||
private bool _haltCharaDataCreation;
|
||||
private bool _isZoning = false;
|
||||
|
||||
@@ -184,18 +183,7 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
if (_isZoning || _haltCharaDataCreation) return;
|
||||
|
||||
bool hasCaches;
|
||||
_cacheCreateLock.Wait();
|
||||
try
|
||||
{
|
||||
hasCaches = _cachesToCreate.Count > 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cacheCreateLock.Release();
|
||||
}
|
||||
|
||||
if (!hasCaches) return;
|
||||
if (_cachesToCreate.Count == 0) return;
|
||||
|
||||
if (_playerRelatedObjects.Any(p => p.Value.CurrentDrawCondition is
|
||||
not (GameObjectHandler.DrawCondition.None or GameObjectHandler.DrawCondition.DrawObjectZero or GameObjectHandler.DrawCondition.ObjectZero)))
|
||||
@@ -209,11 +197,6 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
|
||||
_creationCts = new();
|
||||
_cacheCreateLock.Wait(_creationCts.Token);
|
||||
var objectKindsToCreate = _cachesToCreate.ToList();
|
||||
if (objectKindsToCreate.Count == 0)
|
||||
{
|
||||
_cacheCreateLock.Release();
|
||||
return;
|
||||
}
|
||||
foreach (var creationObj in objectKindsToCreate)
|
||||
{
|
||||
_currentlyCreating.Add(creationObj);
|
||||
@@ -242,17 +225,8 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
|
||||
_playerData.SetFragment(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
var apiData = _playerData.ToAPI();
|
||||
var currentHash = apiData.DataHash.Value;
|
||||
if (string.Equals(_lastPublishedHash, currentHash, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogTrace("Cache creation produced identical character data ({hash}), skipping publish.", currentHash);
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastPublishedHash = currentHash;
|
||||
Mediator.Publish(new CharacterDataCreatedMessage(apiData));
|
||||
}
|
||||
Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI()));
|
||||
_currentlyCreating.Clear();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -264,7 +238,6 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentlyCreating.Clear();
|
||||
Logger.LogDebug("Cache Creation complete");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,20 +13,14 @@ using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.PlayerData.Services;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Chat;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.CharaData;
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.Rendering;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.Services.TextureCompression;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Components;
|
||||
using LightlessSync.UI.Components.Popup;
|
||||
using LightlessSync.UI.Handlers;
|
||||
using LightlessSync.UI.Tags;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.WebAPI;
|
||||
using LightlessSync.WebAPI.Files;
|
||||
using LightlessSync.WebAPI.SignalR;
|
||||
@@ -36,9 +30,6 @@ using Microsoft.Extensions.Logging;
|
||||
using NReco.Logging.File;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using OtterTex;
|
||||
using LightlessSync.Services.LightFinder;
|
||||
using LightlessSync.Services.PairProcessing;
|
||||
|
||||
namespace LightlessSync;
|
||||
|
||||
@@ -52,7 +43,6 @@ public sealed class Plugin : IDalamudPlugin
|
||||
ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig,
|
||||
ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle)
|
||||
{
|
||||
NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName);
|
||||
if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName))
|
||||
Directory.CreateDirectory(pluginInterface.ConfigDirectory.FullName);
|
||||
var traceDir = Path.Join(pluginInterface.ConfigDirectory.FullName, "tracelog");
|
||||
@@ -95,458 +85,191 @@ public sealed class Plugin : IDalamudPlugin
|
||||
});
|
||||
lb.SetMinimumLevel(LogLevel.Trace);
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
var configDir = pluginInterface.ConfigDirectory.FullName;
|
||||
|
||||
// Core infrastructure
|
||||
services.AddSingleton(new WindowSystem("LightlessSync"));
|
||||
services.AddSingleton<FileDialogManager>();
|
||||
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
|
||||
services.AddSingleton(gameGui);
|
||||
services.AddSingleton(addonLifecycle);
|
||||
|
||||
// Core singletons
|
||||
services.AddSingleton<LightlessMediator>();
|
||||
services.AddSingleton<FileCacheManager>();
|
||||
services.AddSingleton<ServerConfigurationManager>();
|
||||
services.AddSingleton<ProfileTagService>();
|
||||
services.AddSingleton<ApiController>();
|
||||
services.AddSingleton<PerformanceCollectorService>();
|
||||
services.AddSingleton<HubFactory>();
|
||||
services.AddSingleton<FileUploadManager>();
|
||||
services.AddSingleton<FileTransferOrchestrator>();
|
||||
services.AddSingleton<LightlessPlugin>();
|
||||
services.AddSingleton<LightlessProfileManager>();
|
||||
services.AddSingleton<TextureCompressionService>();
|
||||
services.AddSingleton<TextureDownscaleService>();
|
||||
services.AddSingleton<GameObjectHandlerFactory>();
|
||||
services.AddSingleton<FileDownloadManagerFactory>();
|
||||
services.AddSingleton<PairProcessingLimiter>();
|
||||
services.AddSingleton<XivDataAnalyzer>();
|
||||
services.AddSingleton<CharacterAnalyzer>();
|
||||
services.AddSingleton<TokenProvider>();
|
||||
services.AddSingleton<PluginWarningNotificationService>();
|
||||
services.AddSingleton<FileCompactor>();
|
||||
services.AddSingleton<TagHandler>();
|
||||
services.AddSingleton<PairRequestService>();
|
||||
services.AddSingleton<ZoneChatService>();
|
||||
services.AddSingleton<IdDisplayHandler>();
|
||||
services.AddSingleton<PlayerPerformanceService>();
|
||||
|
||||
services.AddSingleton<TextureMetadataHelper>(sp =>
|
||||
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
|
||||
|
||||
services.AddSingleton(sp => new Lazy<ApiController>(() => sp.GetRequiredService<ApiController>()));
|
||||
|
||||
services.AddSingleton(sp => new PairFactory(
|
||||
sp.GetRequiredService<ILoggerFactory>(),
|
||||
sp.GetRequiredService<PairLedger>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
new Lazy<ServerConfigurationManager>(() => sp.GetRequiredService<ServerConfigurationManager>()),
|
||||
sp.GetRequiredService<Lazy<ApiController>>()));
|
||||
|
||||
services.AddSingleton(sp => new TransientResourceManager(
|
||||
sp.GetRequiredService<ILogger<TransientResourceManager>>(),
|
||||
sp.GetRequiredService<TransientConfigService>(),
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<ActorObjectService>(),
|
||||
sp.GetRequiredService<GameObjectHandlerFactory>()));
|
||||
|
||||
// Lightless Chara data
|
||||
services.AddSingleton<CharaDataManager>();
|
||||
services.AddSingleton<CharaDataFileHandler>();
|
||||
services.AddSingleton<CharaDataCharacterHandler>();
|
||||
services.AddSingleton<CharaDataNearbyManager>();
|
||||
services.AddSingleton<CharaDataGposeTogetherManager>();
|
||||
|
||||
// Game / VFX / IPC
|
||||
services.AddSingleton(sp => new VfxSpawnManager(
|
||||
sp.GetRequiredService<ILogger<VfxSpawnManager>>(),
|
||||
gameInteropProvider,
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new BlockedCharacterHandler(
|
||||
sp.GetRequiredService<ILogger<BlockedCharacterHandler>>(),
|
||||
gameInteropProvider));
|
||||
|
||||
services.AddSingleton(sp => new IpcProvider(
|
||||
sp.GetRequiredService<ILogger<IpcProvider>>(),
|
||||
pluginInterface,
|
||||
sp.GetRequiredService<CharaDataManager>(),
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new PictomancyService(
|
||||
sp.GetRequiredService<ILogger<PictomancyService>>(),
|
||||
pluginInterface));
|
||||
|
||||
// Tag (Groups) UIs
|
||||
services.AddSingleton<SelectPairForTagUi>();
|
||||
services.AddSingleton<RenamePairTagUi>();
|
||||
services.AddSingleton<SelectSyncshellForTagUi>();
|
||||
services.AddSingleton<RenameSyncshellTagUi>();
|
||||
|
||||
// Eventing / utilities
|
||||
services.AddSingleton(sp => new EventAggregator(
|
||||
configDir,
|
||||
sp.GetRequiredService<ILogger<EventAggregator>>(),
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new ActorObjectService(
|
||||
sp.GetRequiredService<ILogger<ActorObjectService>>(),
|
||||
framework,
|
||||
gameInteropProvider,
|
||||
objectTable,
|
||||
clientState,
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new DalamudUtilService(
|
||||
sp.GetRequiredService<ILogger<DalamudUtilService>>(),
|
||||
clientState,
|
||||
objectTable,
|
||||
framework,
|
||||
gameGui,
|
||||
condition,
|
||||
gameData,
|
||||
targetManager,
|
||||
gameConfig,
|
||||
sp.GetRequiredService<ActorObjectService>(),
|
||||
sp.GetRequiredService<BlockedCharacterHandler>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<PerformanceCollectorService>(),
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<PlayerPerformanceConfigService>(),
|
||||
new Lazy<PairFactory>(() => sp.GetRequiredService<PairFactory>())));
|
||||
|
||||
// Pairing and Dtr integration
|
||||
services.AddSingleton<PairManager>();
|
||||
services.AddSingleton<PairStateCache>();
|
||||
services.AddSingleton<PairPerformanceMetricsCache>();
|
||||
services.AddSingleton<PairLedger>();
|
||||
services.AddSingleton<PairUiService>();
|
||||
services.AddSingleton<IPairHandlerAdapterFactory, PairHandlerAdapterFactory>();
|
||||
|
||||
services.AddSingleton(sp => new PairHandlerRegistry(
|
||||
sp.GetRequiredService<IPairHandlerAdapterFactory>(),
|
||||
sp.GetRequiredService<PairManager>(),
|
||||
sp.GetRequiredService<PairStateCache>(),
|
||||
sp.GetRequiredService<PairPerformanceMetricsCache>(),
|
||||
sp.GetRequiredService<ILogger<PairHandlerRegistry>>()));
|
||||
|
||||
services.AddSingleton(sp => new DtrEntry(
|
||||
sp.GetRequiredService<ILogger<DtrEntry>>(),
|
||||
dtrBar,
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<PairUiService>(),
|
||||
sp.GetRequiredService<PairRequestService>(),
|
||||
sp.GetRequiredService<ApiController>(),
|
||||
sp.GetRequiredService<ServerConfigurationManager>(),
|
||||
sp.GetRequiredService<LightFinderService>(),
|
||||
sp.GetRequiredService<LightFinderScannerService>(),
|
||||
sp.GetRequiredService<DalamudUtilService>()));
|
||||
|
||||
services.AddSingleton(sp => new PairCoordinator(
|
||||
sp.GetRequiredService<ILogger<PairCoordinator>>(),
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<PairHandlerRegistry>(),
|
||||
sp.GetRequiredService<PairManager>(),
|
||||
sp.GetRequiredService<PairLedger>(),
|
||||
sp.GetRequiredService<ServerConfigurationManager>(),
|
||||
sp.GetRequiredService<PairPerformanceMetricsCache>()));
|
||||
|
||||
// Light finder / redraw / context menu
|
||||
services.AddSingleton<RedrawManager>();
|
||||
services.AddSingleton<LightFinderService>();
|
||||
|
||||
services.AddSingleton(sp => new LightFinderPlateHandler(
|
||||
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
|
||||
addonLifecycle,
|
||||
gameGui,
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
objectTable,
|
||||
sp.GetRequiredService<PairUiService>(),
|
||||
pluginInterface,
|
||||
sp.GetRequiredService<PictomancyService>()));
|
||||
|
||||
services.AddSingleton(sp => new LightFinderScannerService(
|
||||
sp.GetRequiredService<ILogger<LightFinderScannerService>>(),
|
||||
framework,
|
||||
sp.GetRequiredService<LightFinderService>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<LightFinderPlateHandler>(),
|
||||
sp.GetRequiredService<ActorObjectService>()));
|
||||
|
||||
services.AddSingleton(sp => new ContextMenuService(
|
||||
contextMenu,
|
||||
pluginInterface,
|
||||
gameData,
|
||||
sp.GetRequiredService<ILogger<ContextMenuService>>(),
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<ApiController>(),
|
||||
objectTable,
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<PairRequestService>(),
|
||||
sp.GetRequiredService<PairUiService>(),
|
||||
clientState,
|
||||
sp.GetRequiredService<LightFinderScannerService>(),
|
||||
sp.GetRequiredService<LightFinderService>(),
|
||||
sp.GetRequiredService<LightlessProfileManager>(),
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
// IPC callers / manager
|
||||
services.AddSingleton(sp => new IpcCallerPenumbra(
|
||||
sp.GetRequiredService<ILogger<IpcCallerPenumbra>>(),
|
||||
pluginInterface,
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<RedrawManager>(),
|
||||
sp.GetRequiredService<ActorObjectService>()));
|
||||
|
||||
services.AddSingleton(sp => new IpcCallerGlamourer(
|
||||
sp.GetRequiredService<ILogger<IpcCallerGlamourer>>(),
|
||||
pluginInterface,
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<RedrawManager>()));
|
||||
|
||||
services.AddSingleton(sp => new IpcCallerCustomize(
|
||||
sp.GetRequiredService<ILogger<IpcCallerCustomize>>(),
|
||||
pluginInterface,
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new IpcCallerHeels(
|
||||
sp.GetRequiredService<ILogger<IpcCallerHeels>>(),
|
||||
pluginInterface,
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new IpcCallerHonorific(
|
||||
sp.GetRequiredService<ILogger<IpcCallerHonorific>>(),
|
||||
pluginInterface,
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new IpcCallerMoodles(
|
||||
sp.GetRequiredService<ILogger<IpcCallerMoodles>>(),
|
||||
pluginInterface,
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new IpcCallerPetNames(
|
||||
sp.GetRequiredService<ILogger<IpcCallerPetNames>>(),
|
||||
pluginInterface,
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new IpcCallerBrio(
|
||||
sp.GetRequiredService<ILogger<IpcCallerBrio>>(),
|
||||
pluginInterface,
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new IpcManager(
|
||||
sp.GetRequiredService<ILogger<IpcManager>>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<IpcCallerPenumbra>(),
|
||||
sp.GetRequiredService<IpcCallerGlamourer>(),
|
||||
sp.GetRequiredService<IpcCallerCustomize>(),
|
||||
sp.GetRequiredService<IpcCallerHeels>(),
|
||||
sp.GetRequiredService<IpcCallerHonorific>(),
|
||||
sp.GetRequiredService<IpcCallerMoodles>(),
|
||||
sp.GetRequiredService<IpcCallerPetNames>(),
|
||||
sp.GetRequiredService<IpcCallerBrio>()));
|
||||
|
||||
// Notifications / HTTP
|
||||
services.AddSingleton(sp => new NotificationService(
|
||||
sp.GetRequiredService<ILogger<NotificationService>>(),
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
notificationManager,
|
||||
chatGui,
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<PairRequestService>(),
|
||||
sp.GetRequiredService<PairUiService>(),
|
||||
sp.GetRequiredService<PairFactory>()));
|
||||
|
||||
services.AddSingleton(sp =>
|
||||
.ConfigureServices(collection =>
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(
|
||||
new ProductInfoHeaderValue("LightlessSync", $"{ver!.Major}.{ver.Minor}.{ver.Build}"));
|
||||
return httpClient;
|
||||
});
|
||||
collection.AddSingleton(new WindowSystem("LightlessSync"));
|
||||
collection.AddSingleton<FileDialogManager>();
|
||||
collection.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", "", useEmbedded: true));
|
||||
collection.AddSingleton(gameGui);
|
||||
|
||||
// Lightless Config services
|
||||
services.AddSingleton(sp => new UiThemeConfigService(configDir));
|
||||
services.AddSingleton(sp => new ChatConfigService(configDir));
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
var cfg = new LightlessConfigService(configDir);
|
||||
var theme = sp.GetRequiredService<UiThemeConfigService>();
|
||||
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
|
||||
return cfg;
|
||||
});
|
||||
services.AddSingleton(sp => new ServerConfigService(configDir));
|
||||
services.AddSingleton(sp => new NotesConfigService(configDir));
|
||||
services.AddSingleton(sp => new PairTagConfigService(configDir));
|
||||
services.AddSingleton(sp => new SyncshellTagConfigService(configDir));
|
||||
services.AddSingleton(sp => new TransientConfigService(configDir));
|
||||
services.AddSingleton(sp => new XivDataStorageService(configDir));
|
||||
services.AddSingleton(sp => new PlayerPerformanceConfigService(configDir));
|
||||
services.AddSingleton(sp => new CharaDataConfigService(configDir));
|
||||
// add lightless related singletons
|
||||
collection.AddSingleton<LightlessMediator>();
|
||||
collection.AddSingleton<FileCacheManager>();
|
||||
collection.AddSingleton<ServerConfigurationManager>();
|
||||
collection.AddSingleton<ApiController>();
|
||||
collection.AddSingleton<PerformanceCollectorService>();
|
||||
collection.AddSingleton<HubFactory>();
|
||||
collection.AddSingleton<FileUploadManager>();
|
||||
collection.AddSingleton<FileTransferOrchestrator>();
|
||||
collection.AddSingleton<LightlessPlugin>();
|
||||
collection.AddSingleton<LightlessProfileManager>();
|
||||
collection.AddSingleton<GameObjectHandlerFactory>();
|
||||
collection.AddSingleton<FileDownloadManagerFactory>();
|
||||
collection.AddSingleton<PairHandlerFactory>();
|
||||
collection.AddSingleton<PairProcessingLimiter>();
|
||||
collection.AddSingleton<PairFactory>();
|
||||
collection.AddSingleton<XivDataAnalyzer>();
|
||||
collection.AddSingleton<CharacterAnalyzer>();
|
||||
collection.AddSingleton<TokenProvider>();
|
||||
collection.AddSingleton<PluginWarningNotificationService>();
|
||||
collection.AddSingleton<FileCompactor>();
|
||||
collection.AddSingleton<TagHandler>();
|
||||
collection.AddSingleton<PairRequestService>();
|
||||
collection.AddSingleton<IdDisplayHandler>();
|
||||
collection.AddSingleton<PlayerPerformanceService>();
|
||||
collection.AddSingleton<TransientResourceManager>();
|
||||
|
||||
// Config adapters
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ServerConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<SyncshellTagConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<TransientConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<XivDataStorageService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PlayerPerformanceConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<CharaDataConfigService>());
|
||||
collection.AddSingleton<CharaDataManager>();
|
||||
collection.AddSingleton<CharaDataFileHandler>();
|
||||
collection.AddSingleton<CharaDataCharacterHandler>();
|
||||
collection.AddSingleton<CharaDataNearbyManager>();
|
||||
collection.AddSingleton<CharaDataGposeTogetherManager>();
|
||||
|
||||
services.AddSingleton<ConfigurationMigrator>();
|
||||
services.AddSingleton<ConfigurationSaveService>();
|
||||
collection.AddSingleton(s => new VfxSpawnManager(s.GetRequiredService<ILogger<VfxSpawnManager>>(),
|
||||
gameInteropProvider, s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddSingleton((s) => new BlockedCharacterHandler(s.GetRequiredService<ILogger<BlockedCharacterHandler>>(), gameInteropProvider));
|
||||
collection.AddSingleton((s) => new IpcProvider(s.GetRequiredService<ILogger<IpcProvider>>(),
|
||||
pluginInterface,
|
||||
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<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<ServerConfigurationManager>()));
|
||||
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
|
||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>()));
|
||||
collection.AddSingleton<RedrawManager>();
|
||||
collection.AddSingleton<BroadcastService>();
|
||||
collection.AddSingleton(addonLifecycle);
|
||||
collection.AddSingleton(p => new ContextMenuService(contextMenu, pluginInterface, gameData,
|
||||
p.GetRequiredService<ILogger<ContextMenuService>>(), p.GetRequiredService<DalamudUtilService>(), p.GetRequiredService<ApiController>(), objectTable,
|
||||
p.GetRequiredService<LightlessConfigService>(), p.GetRequiredService<PairRequestService>(), p.GetRequiredService<PairManager>(), clientState));
|
||||
collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService<ILogger<IpcCallerPenumbra>>(), pluginInterface,
|
||||
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<RedrawManager>()));
|
||||
collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService<ILogger<IpcCallerGlamourer>>(), pluginInterface,
|
||||
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<RedrawManager>()));
|
||||
collection.AddSingleton((s) => new IpcCallerCustomize(s.GetRequiredService<ILogger<IpcCallerCustomize>>(), pluginInterface,
|
||||
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddSingleton((s) => new IpcCallerHeels(s.GetRequiredService<ILogger<IpcCallerHeels>>(), pluginInterface,
|
||||
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddSingleton((s) => new IpcCallerHonorific(s.GetRequiredService<ILogger<IpcCallerHonorific>>(), pluginInterface,
|
||||
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddSingleton((s) => new IpcCallerMoodles(s.GetRequiredService<ILogger<IpcCallerMoodles>>(), pluginInterface,
|
||||
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddSingleton((s) => new IpcCallerPetNames(s.GetRequiredService<ILogger<IpcCallerPetNames>>(), pluginInterface,
|
||||
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddSingleton((s) => new IpcCallerBrio(s.GetRequiredService<ILogger<IpcCallerBrio>>(), pluginInterface,
|
||||
s.GetRequiredService<DalamudUtilService>()));
|
||||
collection.AddSingleton((s) => new IpcManager(s.GetRequiredService<ILogger<IpcManager>>(),
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(),
|
||||
s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(),
|
||||
s.GetRequiredService<IpcCallerMoodles>(), s.GetRequiredService<IpcCallerPetNames>(), s.GetRequiredService<IpcCallerBrio>()));
|
||||
collection.AddSingleton((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(),
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<DalamudUtilService>(),
|
||||
notificationManager, chatGui, s.GetRequiredService<LightlessConfigService>()));
|
||||
collection.AddSingleton((s) =>
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||
return httpClient;
|
||||
});
|
||||
collection.AddSingleton((s) =>
|
||||
{
|
||||
var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName);
|
||||
LightlessSync.UI.Style.MainStyle.Init(cfg);
|
||||
return cfg;
|
||||
});
|
||||
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new PairTagConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new SyncshellTagConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new TransientConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new XivDataStorageService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<LightlessConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PairTagConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<SyncshellTagConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<TransientConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<XivDataStorageService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PlayerPerformanceConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<CharaDataConfigService>());
|
||||
collection.AddSingleton<ConfigurationMigrator>();
|
||||
collection.AddSingleton<ConfigurationSaveService>();
|
||||
collection.AddSingleton<HubFactory>();
|
||||
collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService<ILogger<BroadcastScannerService>>(), clientState, objectTable, framework, s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<NameplateHandler>(), s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessConfigService>()));
|
||||
|
||||
// Scoped factories / UI
|
||||
services.AddScoped<DrawEntityFactory>();
|
||||
services.AddScoped<CacheMonitor>();
|
||||
services.AddScoped<UiFactory>();
|
||||
services.AddScoped<SelectTagForPairUi>();
|
||||
services.AddScoped<SelectTagForSyncshellUi>();
|
||||
services.AddScoped<WindowMediatorSubscriberBase, SettingsUi>();
|
||||
services.AddScoped<WindowMediatorSubscriberBase, CompactUi>();
|
||||
services.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
|
||||
services.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
|
||||
services.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
|
||||
services.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>();
|
||||
services.AddScoped<WindowMediatorSubscriberBase, JoinSyncshellUI>();
|
||||
services.AddScoped<WindowMediatorSubscriberBase, CreateSyncshellUI>();
|
||||
services.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
||||
services.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
|
||||
services.AddScoped<WindowMediatorSubscriberBase, UpdateNotesUi>();
|
||||
services.AddScoped<WindowMediatorSubscriberBase, ZoneChatUi>();
|
||||
|
||||
services.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>(sp => new EditProfileUi(
|
||||
sp.GetRequiredService<ILogger<EditProfileUi>>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<ApiController>(),
|
||||
sp.GetRequiredService<UiSharedService>(),
|
||||
sp.GetRequiredService<FileDialogManager>(),
|
||||
sp.GetRequiredService<LightlessProfileManager>(),
|
||||
sp.GetRequiredService<ProfileTagService>(),
|
||||
sp.GetRequiredService<PerformanceCollectorService>()));
|
||||
// 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>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, JoinSyncshellUI>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, CreateSyncshellUI>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
|
||||
|
||||
services.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>((s) => new EditProfileUi(s.GetRequiredService<ILogger<EditProfileUi>>(),
|
||||
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>();
|
||||
collection.AddScoped<PlayerDataFactory>();
|
||||
collection.AddScoped<VisibleUserDataDistributor>();
|
||||
collection.AddScoped((s) => new UiService(s.GetRequiredService<ILogger<UiService>>(), pluginInterface.UiBuilder, s.GetRequiredService<LightlessConfigService>(),
|
||||
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
|
||||
s.GetRequiredService<UiFactory>(),
|
||||
s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(),
|
||||
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(),
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessConfigService>()));
|
||||
collection.AddScoped((s) => new UiSharedService(s.GetRequiredService<ILogger<UiSharedService>>(), s.GetRequiredService<IpcManager>(), s.GetRequiredService<ApiController>(),
|
||||
s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<DalamudUtilService>(),
|
||||
pluginInterface, textureProvider, s.GetRequiredService<Dalamud.Localization>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<TokenProvider>(),
|
||||
s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState,
|
||||
s.GetRequiredService<PairManager>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService<ILogger<NameplateHandler>>(), addonLifecycle, gameGui, s.GetRequiredService<DalamudUtilService>(),
|
||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), clientState, s.GetRequiredService<PairManager>()));
|
||||
|
||||
services.AddScoped<WindowMediatorSubscriberBase, LightFinderUI>(sp => new LightFinderUI(
|
||||
sp.GetRequiredService<ILogger<LightFinderUI>>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<PerformanceCollectorService>(),
|
||||
sp.GetRequiredService<LightFinderService>(),
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<UiSharedService>(),
|
||||
sp.GetRequiredService<ApiController>(),
|
||||
sp.GetRequiredService<LightFinderScannerService>()));
|
||||
|
||||
services.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>(sp => new SyncshellFinderUI(
|
||||
sp.GetRequiredService<ILogger<SyncshellFinderUI>>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<PerformanceCollectorService>(),
|
||||
sp.GetRequiredService<LightFinderService>(),
|
||||
sp.GetRequiredService<UiSharedService>(),
|
||||
sp.GetRequiredService<ApiController>(),
|
||||
sp.GetRequiredService<LightFinderScannerService>(),
|
||||
sp.GetRequiredService<PairUiService>(),
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
sp.GetRequiredService<LightlessProfileManager>()));
|
||||
|
||||
services.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||
services.AddScoped<IPopupHandler, CensusPopupHandler>();
|
||||
|
||||
services.AddScoped<WindowMediatorSubscriberBase, LightlessNotificationUi>(sp =>
|
||||
new LightlessNotificationUi(
|
||||
sp.GetRequiredService<ILogger<LightlessNotificationUi>>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<PerformanceCollectorService>(),
|
||||
sp.GetRequiredService<LightlessConfigService>()));
|
||||
|
||||
services.AddScoped<CacheCreationService>();
|
||||
services.AddScoped<PlayerDataFactory>();
|
||||
services.AddScoped<VisibleUserDataDistributor>();
|
||||
|
||||
services.AddScoped(sp => new UiService(
|
||||
sp.GetRequiredService<ILogger<UiService>>(),
|
||||
pluginInterface.UiBuilder,
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<WindowSystem>(),
|
||||
sp.GetServices<WindowMediatorSubscriberBase>(),
|
||||
sp.GetRequiredService<UiFactory>(),
|
||||
sp.GetRequiredService<FileDialogManager>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<PairFactory>()));
|
||||
|
||||
services.AddScoped(sp => new CommandManagerService(
|
||||
commandManager,
|
||||
sp.GetRequiredService<PerformanceCollectorService>(),
|
||||
sp.GetRequiredService<ServerConfigurationManager>(),
|
||||
sp.GetRequiredService<CacheMonitor>(),
|
||||
sp.GetRequiredService<ApiController>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<LightlessConfigService>()));
|
||||
|
||||
services.AddScoped(sp => new UiSharedService(
|
||||
sp.GetRequiredService<ILogger<UiSharedService>>(),
|
||||
sp.GetRequiredService<IpcManager>(),
|
||||
sp.GetRequiredService<ApiController>(),
|
||||
sp.GetRequiredService<CacheMonitor>(),
|
||||
sp.GetRequiredService<FileDialogManager>(),
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<DalamudUtilService>(),
|
||||
pluginInterface,
|
||||
textureProvider,
|
||||
sp.GetRequiredService<Dalamud.Localization>(),
|
||||
sp.GetRequiredService<ServerConfigurationManager>(),
|
||||
sp.GetRequiredService<TokenProvider>(),
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddScoped(sp => new NameplateService(
|
||||
sp.GetRequiredService<ILogger<NameplateService>>(),
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
clientState,
|
||||
gameGui,
|
||||
objectTable,
|
||||
gameInteropProvider,
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<PairUiService>()));
|
||||
|
||||
// Hosted services
|
||||
services.AddHostedService(sp => sp.GetRequiredService<ConfigurationSaveService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<ActorObjectService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<LightlessMediator>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<ZoneChatService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<NotificationService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<FileCacheManager>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<ConfigurationMigrator>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<DalamudUtilService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<PerformanceCollectorService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<DtrEntry>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<EventAggregator>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<IpcProvider>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<LightlessPlugin>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<ContextMenuService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<LightFinderService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<LightFinderPlateHandler>());
|
||||
}).Build();
|
||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<LightlessMediator>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<NotificationService>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<FileCacheManager>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<DalamudUtilService>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<PerformanceCollectorService>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<DtrEntry>());
|
||||
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();
|
||||
|
||||
_ = _host.StartAsync();
|
||||
}
|
||||
@@ -556,4 +279,4 @@ public sealed class Plugin : IDalamudPlugin
|
||||
_host.StopAsync().GetAwaiter().GetResult();
|
||||
_host.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,938 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.Interop;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
|
||||
namespace LightlessSync.Services.ActorTracking;
|
||||
|
||||
public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
{
|
||||
public readonly record struct ActorDescriptor(
|
||||
string Name,
|
||||
string HashedContentId,
|
||||
nint Address,
|
||||
ushort ObjectIndex,
|
||||
bool IsLocalPlayer,
|
||||
bool IsInGpose,
|
||||
DalamudObjectKind ObjectKind,
|
||||
LightlessObjectKind? OwnedKind,
|
||||
uint OwnerEntityId);
|
||||
|
||||
private readonly ILogger<ActorObjectService> _logger;
|
||||
private readonly IFramework _framework;
|
||||
private readonly IGameInteropProvider _interop;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly LightlessMediator _mediator;
|
||||
|
||||
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
|
||||
private readonly ConcurrentDictionary<string, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal);
|
||||
private readonly OwnedObjectTracker _ownedTracker = new();
|
||||
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
|
||||
|
||||
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
|
||||
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
|
||||
private Hook<Character.Delegates.Dtor>? _onDestructorHook;
|
||||
private Hook<Companion.Delegates.OnInitialize>? _onCompanionInitializeHook;
|
||||
private Hook<Companion.Delegates.Terminate>? _onCompanionTerminateHook;
|
||||
|
||||
private bool _hooksActive;
|
||||
private static readonly TimeSpan SnapshotRefreshInterval = TimeSpan.FromSeconds(1);
|
||||
private DateTime _nextRefreshAllowed = DateTime.MinValue;
|
||||
|
||||
public ActorObjectService(
|
||||
ILogger<ActorObjectService> logger,
|
||||
IFramework framework,
|
||||
IGameInteropProvider interop,
|
||||
IObjectTable objectTable,
|
||||
IClientState clientState,
|
||||
LightlessMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_framework = framework;
|
||||
_interop = interop;
|
||||
_objectTable = objectTable;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
||||
|
||||
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
|
||||
|
||||
public IEnumerable<ActorDescriptor> PlayerDescriptors => _activePlayers.Values;
|
||||
public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Snapshot.PlayerDescriptors;
|
||||
|
||||
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
|
||||
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
||||
{
|
||||
descriptor = default;
|
||||
if (!_actorsByHash.TryGetValue(hash, out var candidate))
|
||||
return false;
|
||||
|
||||
if (!ValidateDescriptorThreadSafe(candidate))
|
||||
return false;
|
||||
|
||||
descriptor = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetPlayerByName(string name, out ActorDescriptor descriptor)
|
||||
{
|
||||
descriptor = default;
|
||||
|
||||
if (!_actorsByName.TryGetValue(name, out var entries) || entries.IsEmpty)
|
||||
return false;
|
||||
|
||||
ActorDescriptor? best = null;
|
||||
foreach (var candidate in entries.Values)
|
||||
{
|
||||
if (!ValidateDescriptorThreadSafe(candidate))
|
||||
continue;
|
||||
|
||||
if (best is null || IsBetterNameMatch(candidate, best.Value))
|
||||
{
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (best is { } selected)
|
||||
{
|
||||
descriptor = selected;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
public bool HooksActive => _hooksActive;
|
||||
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
|
||||
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
|
||||
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
|
||||
public IReadOnlyDictionary<nint, LightlessObjectKind> OwnedObjects => Snapshot.OwnedObjects.Map;
|
||||
public nint LocalPlayerAddress => Snapshot.OwnedObjects.LocalPlayer;
|
||||
public nint LocalPetAddress => Snapshot.OwnedObjects.LocalPet;
|
||||
public nint LocalMinionOrMountAddress => Snapshot.OwnedObjects.LocalMinionOrMount;
|
||||
public nint LocalCompanionAddress => Snapshot.OwnedObjects.LocalCompanion;
|
||||
|
||||
public bool TryGetOwnedKind(nint address, out LightlessObjectKind kind)
|
||||
=> OwnedObjects.TryGetValue(address, out kind);
|
||||
|
||||
public bool TryGetOwnedActor(LightlessObjectKind kind, out ActorDescriptor descriptor)
|
||||
{
|
||||
descriptor = default;
|
||||
if (!TryGetOwnedObject(kind, out var address))
|
||||
return false;
|
||||
return TryGetDescriptor(address, out descriptor);
|
||||
}
|
||||
|
||||
public bool TryGetOwnedObjectByIndex(ushort objectIndex, out LightlessObjectKind ownedKind)
|
||||
{
|
||||
ownedKind = default;
|
||||
var ownedSnapshot = OwnedObjects;
|
||||
foreach (var (address, kind) in ownedSnapshot)
|
||||
{
|
||||
if (!TryGetDescriptor(address, out var descriptor))
|
||||
continue;
|
||||
|
||||
if (descriptor.ObjectIndex == objectIndex)
|
||||
{
|
||||
ownedKind = kind;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetOwnedObject(LightlessObjectKind kind, out nint address)
|
||||
{
|
||||
var ownedSnapshot = Snapshot.OwnedObjects;
|
||||
address = kind switch
|
||||
{
|
||||
LightlessObjectKind.Player => ownedSnapshot.LocalPlayer,
|
||||
LightlessObjectKind.Pet => ownedSnapshot.LocalPet,
|
||||
LightlessObjectKind.MinionOrMount => ownedSnapshot.LocalMinionOrMount,
|
||||
LightlessObjectKind.Companion => ownedSnapshot.LocalCompanion,
|
||||
_ => nint.Zero
|
||||
};
|
||||
|
||||
return address != nint.Zero;
|
||||
}
|
||||
|
||||
public bool TryGetOwnedActor(uint ownerEntityId, LightlessObjectKind? kindFilter, out ActorDescriptor descriptor)
|
||||
{
|
||||
descriptor = default;
|
||||
foreach (var candidate in _activePlayers.Values)
|
||||
{
|
||||
if (candidate.OwnerEntityId != ownerEntityId)
|
||||
continue;
|
||||
|
||||
if (kindFilter.HasValue && candidate.OwnedKind != kindFilter)
|
||||
continue;
|
||||
|
||||
descriptor = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetPlayerAddressByHash(string hash, out nint address)
|
||||
{
|
||||
if (TryGetValidatedActorByHash(hash, out var descriptor) && descriptor.Address != nint.Zero)
|
||||
{
|
||||
address = descriptor.Address;
|
||||
return true;
|
||||
}
|
||||
|
||||
address = nint.Zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
throw new ArgumentException("Address cannot be zero.", nameof(address));
|
||||
|
||||
while (true)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
||||
if (isLoaded)
|
||||
return;
|
||||
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateDescriptorThreadSafe(ActorDescriptor descriptor)
|
||||
{
|
||||
if (_framework.IsInFrameworkUpdateThread)
|
||||
return ValidateDescriptorInternal(descriptor);
|
||||
|
||||
return _framework.RunOnFrameworkThread(() => ValidateDescriptorInternal(descriptor)).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private bool ValidateDescriptorInternal(ActorDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.Address == nint.Zero)
|
||||
return false;
|
||||
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player &&
|
||||
!string.IsNullOrEmpty(descriptor.HashedContentId))
|
||||
{
|
||||
if (!TryGetLivePlayerHash(descriptor, out var liveHash))
|
||||
{
|
||||
UntrackGameObject(descriptor.Address);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(liveHash, descriptor.HashedContentId, StringComparison.Ordinal))
|
||||
{
|
||||
UntrackGameObject(descriptor.Address);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetLivePlayerHash(ActorDescriptor descriptor, out string liveHash)
|
||||
{
|
||||
liveHash = string.Empty;
|
||||
|
||||
if (_objectTable.CreateObjectReference(descriptor.Address) is not IPlayerCharacter playerCharacter)
|
||||
return false;
|
||||
|
||||
return DalamudUtilService.TryGetHashedCID(playerCharacter, out liveHash);
|
||||
}
|
||||
|
||||
public void RefreshTrackedActors(bool force = false)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (!force && _hooksActive)
|
||||
{
|
||||
if (now < _nextRefreshAllowed)
|
||||
return;
|
||||
|
||||
_nextRefreshAllowed = now + SnapshotRefreshInterval;
|
||||
}
|
||||
|
||||
if (_framework.IsInFrameworkUpdateThread)
|
||||
{
|
||||
RefreshTrackedActorsInternal();
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = _framework.RunOnFrameworkThread(RefreshTrackedActorsInternal);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeHooks();
|
||||
var warmupTask = WarmupExistingActors();
|
||||
return warmupTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to initialize ActorObjectService hooks, falling back to empty cache.");
|
||||
DisposeHooks();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
DisposeHooks();
|
||||
_activePlayers.Clear();
|
||||
_actorsByHash.Clear();
|
||||
_actorsByName.Clear();
|
||||
_ownedTracker.Reset();
|
||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private unsafe void InitializeHooks()
|
||||
{
|
||||
if (_hooksActive)
|
||||
return;
|
||||
|
||||
_onInitializeHook = _interop.HookFromAddress<Character.Delegates.OnInitialize>(
|
||||
(nint)Character.StaticVirtualTablePointer->OnInitialize,
|
||||
OnCharacterInitialized);
|
||||
|
||||
_onTerminateHook = _interop.HookFromAddress<Character.Delegates.Terminate>(
|
||||
(nint)Character.StaticVirtualTablePointer->Terminate,
|
||||
OnCharacterTerminated);
|
||||
|
||||
_onDestructorHook = _interop.HookFromAddress<Character.Delegates.Dtor>(
|
||||
(nint)Character.StaticVirtualTablePointer->Dtor,
|
||||
OnCharacterDisposed);
|
||||
|
||||
_onCompanionInitializeHook = _interop.HookFromAddress<Companion.Delegates.OnInitialize>(
|
||||
(nint)Companion.StaticVirtualTablePointer->OnInitialize,
|
||||
OnCompanionInitialized);
|
||||
|
||||
_onCompanionTerminateHook = _interop.HookFromAddress<Companion.Delegates.Terminate>(
|
||||
(nint)Companion.StaticVirtualTablePointer->Terminate,
|
||||
OnCompanionTerminated);
|
||||
|
||||
_onInitializeHook.Enable();
|
||||
_onTerminateHook.Enable();
|
||||
_onDestructorHook.Enable();
|
||||
_onCompanionInitializeHook.Enable();
|
||||
_onCompanionTerminateHook.Enable();
|
||||
|
||||
_hooksActive = true;
|
||||
_logger.LogDebug("ActorObjectService hooks enabled.");
|
||||
}
|
||||
|
||||
private Task WarmupExistingActors()
|
||||
{
|
||||
return _framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
RefreshTrackedActorsInternal();
|
||||
_nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
|
||||
});
|
||||
}
|
||||
|
||||
private unsafe void OnCharacterInitialized(Character* chara)
|
||||
{
|
||||
try
|
||||
{
|
||||
_onInitializeHook!.Original(chara);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original character initialize.");
|
||||
}
|
||||
|
||||
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara));
|
||||
}
|
||||
|
||||
private unsafe void OnCharacterTerminated(Character* chara)
|
||||
{
|
||||
var address = (nint)chara;
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
try
|
||||
{
|
||||
_onTerminateHook!.Original(chara);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original character terminate.");
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
||||
{
|
||||
var address = (nint)chara;
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
try
|
||||
{
|
||||
return _onDestructorHook!.Original(chara, freeMemory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original character destructor.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void TrackGameObject(GameObject* gameObject)
|
||||
{
|
||||
if (gameObject == null)
|
||||
return;
|
||||
|
||||
var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
|
||||
|
||||
if (!IsSupportedObjectKind(objectKind))
|
||||
return;
|
||||
|
||||
if (BuildDescriptor(gameObject, objectKind) is not { } descriptor)
|
||||
return;
|
||||
|
||||
if (descriptor.ObjectKind != DalamudObjectKind.Player && descriptor.OwnedKind is null)
|
||||
return;
|
||||
|
||||
if (_activePlayers.TryGetValue(descriptor.Address, out var existing))
|
||||
{
|
||||
RemoveDescriptor(existing);
|
||||
}
|
||||
|
||||
AddDescriptor(descriptor);
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
|
||||
descriptor.Name,
|
||||
descriptor.Address,
|
||||
descriptor.ObjectIndex,
|
||||
descriptor.OwnedKind?.ToString() ?? "<none>",
|
||||
descriptor.IsLocalPlayer,
|
||||
descriptor.IsInGpose);
|
||||
}
|
||||
|
||||
_mediator.Publish(new ActorTrackedMessage(descriptor));
|
||||
}
|
||||
|
||||
private unsafe ActorDescriptor? BuildDescriptor(GameObject* gameObject, DalamudObjectKind objectKind)
|
||||
{
|
||||
if (gameObject == null)
|
||||
return null;
|
||||
|
||||
var address = (nint)gameObject;
|
||||
string name = string.Empty;
|
||||
ushort objectIndex = gameObject->ObjectIndex;
|
||||
bool isInGpose = objectIndex >= 200;
|
||||
bool isLocal = _objectTable.LocalPlayer?.Address == address;
|
||||
string hashedCid = string.Empty;
|
||||
|
||||
IPlayerCharacter? resolvedPlayer = null;
|
||||
if (_objectTable.CreateObjectReference(address) is IPlayerCharacter playerCharacter)
|
||||
{
|
||||
resolvedPlayer = playerCharacter;
|
||||
name = playerCharacter.Name.TextValue ?? string.Empty;
|
||||
objectIndex = playerCharacter.ObjectIndex;
|
||||
isInGpose = objectIndex >= 200;
|
||||
isLocal = playerCharacter.Address == _objectTable.LocalPlayer?.Address;
|
||||
}
|
||||
else
|
||||
{
|
||||
name = gameObject->NameString ?? string.Empty;
|
||||
}
|
||||
|
||||
if (objectKind == DalamudObjectKind.Player)
|
||||
{
|
||||
if (resolvedPlayer == null || !DalamudUtilService.TryGetHashedCID(resolvedPlayer, out hashedCid))
|
||||
{
|
||||
hashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address);
|
||||
}
|
||||
}
|
||||
|
||||
var (ownedKind, ownerEntityId) = DetermineOwnedKind(gameObject, objectKind, isLocal);
|
||||
|
||||
return new ActorDescriptor(name, hashedCid, address, objectIndex, isLocal, isInGpose, objectKind, ownedKind, ownerEntityId);
|
||||
}
|
||||
|
||||
private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer)
|
||||
{
|
||||
if (gameObject == null)
|
||||
return (null, 0);
|
||||
|
||||
if (objectKind == DalamudObjectKind.Player)
|
||||
{
|
||||
var entityId = ((Character*)gameObject)->EntityId;
|
||||
return (isLocalPlayer ? LightlessObjectKind.Player : null, entityId);
|
||||
}
|
||||
|
||||
if (isLocalPlayer)
|
||||
{
|
||||
var entityId = ((Character*)gameObject)->EntityId;
|
||||
return (LightlessObjectKind.Player, entityId);
|
||||
}
|
||||
|
||||
if (_objectTable.LocalPlayer is not { } localPlayer)
|
||||
return (null, 0);
|
||||
|
||||
var ownerId = gameObject->OwnerId;
|
||||
if (ownerId == 0)
|
||||
{
|
||||
var character = (Character*)gameObject;
|
||||
if (character != null)
|
||||
{
|
||||
ownerId = character->CompanionOwnerId;
|
||||
if (ownerId == 0)
|
||||
{
|
||||
var parent = character->GetParentCharacter();
|
||||
if (parent != null)
|
||||
{
|
||||
ownerId = parent->EntityId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ownerId == 0 || ownerId != localPlayer.EntityId)
|
||||
return (null, ownerId);
|
||||
|
||||
var ownedKind = objectKind switch
|
||||
{
|
||||
DalamudObjectKind.MountType => LightlessObjectKind.MinionOrMount,
|
||||
DalamudObjectKind.Companion => LightlessObjectKind.MinionOrMount,
|
||||
DalamudObjectKind.BattleNpc => gameObject->BattleNpcSubKind switch
|
||||
{
|
||||
BattleNpcSubKind.Buddy => LightlessObjectKind.Companion,
|
||||
BattleNpcSubKind.Pet => LightlessObjectKind.Pet,
|
||||
_ => (LightlessObjectKind?)null,
|
||||
},
|
||||
_ => (LightlessObjectKind?)null,
|
||||
};
|
||||
|
||||
return (ownedKind, ownerId);
|
||||
}
|
||||
|
||||
private void UntrackGameObject(nint address)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
return;
|
||||
|
||||
if (_activePlayers.TryRemove(address, out var descriptor))
|
||||
{
|
||||
RemoveDescriptor(descriptor);
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
|
||||
descriptor.Name,
|
||||
descriptor.Address,
|
||||
descriptor.ObjectIndex,
|
||||
descriptor.OwnedKind?.ToString() ?? "<none>");
|
||||
}
|
||||
|
||||
_mediator.Publish(new ActorUntrackedMessage(descriptor));
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void RefreshTrackedActorsInternal()
|
||||
{
|
||||
var addresses = EnumerateActiveCharacterAddresses();
|
||||
HashSet<nint> seen = new(addresses.Count);
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
continue;
|
||||
|
||||
if (!seen.Add(address))
|
||||
continue;
|
||||
|
||||
if (_activePlayers.ContainsKey(address))
|
||||
continue;
|
||||
|
||||
TrackGameObject((GameObject*)address);
|
||||
}
|
||||
|
||||
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
||||
foreach (var staleAddress in stale)
|
||||
{
|
||||
UntrackGameObject(staleAddress);
|
||||
}
|
||||
|
||||
if (_hooksActive)
|
||||
{
|
||||
_nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
|
||||
}
|
||||
}
|
||||
|
||||
private void IndexDescriptor(ActorDescriptor descriptor)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(descriptor.HashedContentId))
|
||||
{
|
||||
_actorsByHash[descriptor.HashedContentId] = descriptor;
|
||||
}
|
||||
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player && !string.IsNullOrEmpty(descriptor.Name))
|
||||
{
|
||||
var bucket = _actorsByName.GetOrAdd(descriptor.Name, _ => new ConcurrentDictionary<nint, ActorDescriptor>());
|
||||
bucket[descriptor.Address] = descriptor;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsBetterNameMatch(ActorDescriptor candidate, ActorDescriptor current)
|
||||
{
|
||||
if (!candidate.IsInGpose && current.IsInGpose)
|
||||
return true;
|
||||
if (candidate.IsInGpose && !current.IsInGpose)
|
||||
return false;
|
||||
|
||||
return candidate.ObjectIndex < current.ObjectIndex;
|
||||
}
|
||||
|
||||
private bool TryGetDescriptor(nint address, out ActorDescriptor descriptor)
|
||||
=> _activePlayers.TryGetValue(address, out descriptor);
|
||||
|
||||
private unsafe void OnCompanionInitialized(Companion* companion)
|
||||
{
|
||||
try
|
||||
{
|
||||
_onCompanionInitializeHook!.Original(companion);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original companion initialize.");
|
||||
}
|
||||
|
||||
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion));
|
||||
}
|
||||
|
||||
private unsafe void OnCompanionTerminated(Companion* companion)
|
||||
{
|
||||
var address = (nint)companion;
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
try
|
||||
{
|
||||
_onCompanionTerminateHook!.Original(companion);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original companion terminate.");
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(descriptor.HashedContentId))
|
||||
{
|
||||
_actorsByHash.TryRemove(descriptor.HashedContentId, out _);
|
||||
}
|
||||
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player
|
||||
&& !string.IsNullOrEmpty(descriptor.Name)
|
||||
&& _actorsByName.TryGetValue(descriptor.Name, out var bucket))
|
||||
{
|
||||
bucket.TryRemove(descriptor.Address, out _);
|
||||
if (bucket.IsEmpty)
|
||||
{
|
||||
_actorsByName.TryRemove(descriptor.Name, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDescriptor(ActorDescriptor descriptor)
|
||||
{
|
||||
_activePlayers[descriptor.Address] = descriptor;
|
||||
IndexDescriptor(descriptor);
|
||||
_ownedTracker.OnDescriptorAdded(descriptor);
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
private void RemoveDescriptor(ActorDescriptor descriptor)
|
||||
{
|
||||
RemoveDescriptorFromIndexes(descriptor);
|
||||
_ownedTracker.OnDescriptorRemoved(descriptor);
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
private void PublishSnapshot()
|
||||
{
|
||||
var playerDescriptors = _activePlayers.Values
|
||||
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
.ToArray();
|
||||
var playerAddresses = new nint[playerDescriptors.Length];
|
||||
for (var i = 0; i < playerDescriptors.Length; i++)
|
||||
playerAddresses[i] = playerDescriptors[i].Address;
|
||||
|
||||
var ownedSnapshot = _ownedTracker.CreateSnapshot();
|
||||
var nextGeneration = Snapshot.Generation + 1;
|
||||
var snapshot = new ActorSnapshot(playerDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
|
||||
Volatile.Write(ref _snapshot, snapshot);
|
||||
}
|
||||
|
||||
private void QueueFrameworkUpdate(Action action)
|
||||
{
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
if (_framework.IsInFrameworkUpdateThread)
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
|
||||
_ = _framework.RunOnFrameworkThread(action);
|
||||
}
|
||||
|
||||
private void DisposeHooks()
|
||||
{
|
||||
var hadHooks = _hooksActive
|
||||
|| _onInitializeHook is not null
|
||||
|| _onTerminateHook is not null
|
||||
|| _onDestructorHook is not null
|
||||
|| _onCompanionInitializeHook is not null
|
||||
|| _onCompanionTerminateHook is not null;
|
||||
|
||||
_onInitializeHook?.Disable();
|
||||
_onTerminateHook?.Disable();
|
||||
_onDestructorHook?.Disable();
|
||||
_onCompanionInitializeHook?.Disable();
|
||||
_onCompanionTerminateHook?.Disable();
|
||||
|
||||
_onInitializeHook?.Dispose();
|
||||
_onTerminateHook?.Dispose();
|
||||
_onDestructorHook?.Dispose();
|
||||
_onCompanionInitializeHook?.Dispose();
|
||||
_onCompanionTerminateHook?.Dispose();
|
||||
|
||||
_onInitializeHook = null;
|
||||
_onTerminateHook = null;
|
||||
_onDestructorHook = null;
|
||||
_onCompanionInitializeHook = null;
|
||||
_onCompanionTerminateHook = null;
|
||||
|
||||
_hooksActive = false;
|
||||
|
||||
if (hadHooks)
|
||||
{
|
||||
_logger.LogDebug("ActorObjectService hooks disabled.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeHooks();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private static bool IsSupportedObjectKind(DalamudObjectKind objectKind) =>
|
||||
objectKind is DalamudObjectKind.Player
|
||||
or DalamudObjectKind.BattleNpc
|
||||
or DalamudObjectKind.Companion
|
||||
or DalamudObjectKind.MountType;
|
||||
|
||||
private static unsafe List<nint> EnumerateActiveCharacterAddresses()
|
||||
{
|
||||
var results = new List<nint>(64);
|
||||
var manager = GameObjectManager.Instance();
|
||||
if (manager == null)
|
||||
return results;
|
||||
|
||||
const int objectLimit = 200;
|
||||
|
||||
unsafe
|
||||
{
|
||||
for (var i = 0; i < objectLimit; i++)
|
||||
{
|
||||
Pointer<GameObject> objPtr = manager->Objects.IndexSorted[i];
|
||||
var obj = objPtr.Value;
|
||||
if (obj == null)
|
||||
continue;
|
||||
|
||||
var objectKind = (DalamudObjectKind)obj->ObjectKind;
|
||||
if (!IsSupportedObjectKind(objectKind))
|
||||
continue;
|
||||
|
||||
results.Add((nint)obj);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
return false;
|
||||
|
||||
var gameObject = (GameObject*)address;
|
||||
if (gameObject == null)
|
||||
return false;
|
||||
|
||||
var drawObject = gameObject->DrawObject;
|
||||
if (drawObject == null)
|
||||
return false;
|
||||
|
||||
if ((gameObject->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None)
|
||||
return false;
|
||||
|
||||
var characterBase = (CharacterBase*)drawObject;
|
||||
if (characterBase == null)
|
||||
return false;
|
||||
|
||||
if (characterBase->HasModelInSlotLoaded != 0)
|
||||
return false;
|
||||
|
||||
if (characterBase->HasModelFilesInSlotLoaded != 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private sealed class OwnedObjectTracker
|
||||
{
|
||||
private readonly HashSet<nint> _renderedPlayers = new();
|
||||
private readonly HashSet<nint> _renderedCompanions = new();
|
||||
private readonly Dictionary<nint, LightlessObjectKind> _ownedObjects = new();
|
||||
private nint _localPlayerAddress = nint.Zero;
|
||||
private nint _localPetAddress = nint.Zero;
|
||||
private nint _localMinionMountAddress = nint.Zero;
|
||||
private nint _localCompanionAddress = nint.Zero;
|
||||
|
||||
public void OnDescriptorAdded(ActorDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
{
|
||||
_renderedPlayers.Add(descriptor.Address);
|
||||
if (descriptor.IsLocalPlayer)
|
||||
_localPlayerAddress = descriptor.Address;
|
||||
}
|
||||
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
|
||||
{
|
||||
_renderedCompanions.Add(descriptor.Address);
|
||||
}
|
||||
|
||||
if (descriptor.OwnedKind is { } ownedKind)
|
||||
{
|
||||
_ownedObjects[descriptor.Address] = ownedKind;
|
||||
switch (ownedKind)
|
||||
{
|
||||
case LightlessObjectKind.Player:
|
||||
_localPlayerAddress = descriptor.Address;
|
||||
break;
|
||||
case LightlessObjectKind.Pet:
|
||||
_localPetAddress = descriptor.Address;
|
||||
break;
|
||||
case LightlessObjectKind.MinionOrMount:
|
||||
_localMinionMountAddress = descriptor.Address;
|
||||
break;
|
||||
case LightlessObjectKind.Companion:
|
||||
_localCompanionAddress = descriptor.Address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDescriptorRemoved(ActorDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
{
|
||||
_renderedPlayers.Remove(descriptor.Address);
|
||||
if (descriptor.IsLocalPlayer && _localPlayerAddress == descriptor.Address)
|
||||
_localPlayerAddress = nint.Zero;
|
||||
}
|
||||
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
|
||||
{
|
||||
_renderedCompanions.Remove(descriptor.Address);
|
||||
if (_localCompanionAddress == descriptor.Address)
|
||||
_localCompanionAddress = nint.Zero;
|
||||
}
|
||||
|
||||
if (descriptor.OwnedKind is { } ownedKind)
|
||||
{
|
||||
_ownedObjects.Remove(descriptor.Address);
|
||||
switch (ownedKind)
|
||||
{
|
||||
case LightlessObjectKind.Player when _localPlayerAddress == descriptor.Address:
|
||||
_localPlayerAddress = nint.Zero;
|
||||
break;
|
||||
case LightlessObjectKind.Pet when _localPetAddress == descriptor.Address:
|
||||
_localPetAddress = nint.Zero;
|
||||
break;
|
||||
case LightlessObjectKind.MinionOrMount when _localMinionMountAddress == descriptor.Address:
|
||||
_localMinionMountAddress = nint.Zero;
|
||||
break;
|
||||
case LightlessObjectKind.Companion when _localCompanionAddress == descriptor.Address:
|
||||
_localCompanionAddress = nint.Zero;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public OwnedObjectSnapshot CreateSnapshot()
|
||||
=> new(
|
||||
_renderedPlayers.ToArray(),
|
||||
_renderedCompanions.ToArray(),
|
||||
_ownedObjects.Keys.ToArray(),
|
||||
new Dictionary<nint, LightlessObjectKind>(_ownedObjects),
|
||||
_localPlayerAddress,
|
||||
_localPetAddress,
|
||||
_localMinionMountAddress,
|
||||
_localCompanionAddress);
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_renderedPlayers.Clear();
|
||||
_renderedCompanions.Clear();
|
||||
_ownedObjects.Clear();
|
||||
_localPlayerAddress = nint.Zero;
|
||||
_localPetAddress = nint.Zero;
|
||||
_localMinionMountAddress = nint.Zero;
|
||||
_localCompanionAddress = nint.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record OwnedObjectSnapshot(
|
||||
IReadOnlyList<nint> RenderedPlayers,
|
||||
IReadOnlyList<nint> RenderedCompanions,
|
||||
IReadOnlyList<nint> OwnedAddresses,
|
||||
IReadOnlyDictionary<nint, LightlessObjectKind> Map,
|
||||
nint LocalPlayer,
|
||||
nint LocalPet,
|
||||
nint LocalMinionOrMount,
|
||||
nint LocalCompanion)
|
||||
{
|
||||
public static OwnedObjectSnapshot Empty { get; } = new(
|
||||
Array.Empty<nint>(),
|
||||
Array.Empty<nint>(),
|
||||
Array.Empty<nint>(),
|
||||
new Dictionary<nint, LightlessObjectKind>(),
|
||||
nint.Zero,
|
||||
nint.Zero,
|
||||
nint.Zero,
|
||||
nint.Zero);
|
||||
}
|
||||
|
||||
private sealed record ActorSnapshot(
|
||||
IReadOnlyList<ActorDescriptor> PlayerDescriptors,
|
||||
IReadOnlyList<nint> PlayerAddresses,
|
||||
OwnedObjectSnapshot OwnedObjects,
|
||||
int Generation)
|
||||
{
|
||||
public static ActorSnapshot Empty { get; } = new(
|
||||
Array.Empty<ActorDescriptor>(),
|
||||
Array.Empty<nint>(),
|
||||
OwnedObjectSnapshot.Empty,
|
||||
0);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,67 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace LightlessSync.Services.LightFinder;
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDisposable
|
||||
{
|
||||
private readonly ILogger<LightFinderScannerService> _logger;
|
||||
private readonly ActorObjectService _actorTracker;
|
||||
private readonly ILogger<BroadcastScannerService> _logger;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly IFramework _framework;
|
||||
|
||||
private readonly LightFinderService _broadcastService;
|
||||
private readonly LightFinderPlateHandler _lightFinderPlateHandler;
|
||||
private readonly BroadcastService _broadcastService;
|
||||
private readonly NameplateHandler _nameplateHandler;
|
||||
|
||||
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new();
|
||||
private readonly Queue<string> _lookupQueue = new();
|
||||
private readonly HashSet<string> _lookupQueuedCids = [];
|
||||
private readonly HashSet<string> _syncshellCids = [];
|
||||
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 static readonly TimeSpan MaxAllowedTtl = TimeSpan.FromMinutes(4);
|
||||
private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1);
|
||||
|
||||
private readonly CancellationTokenSource _cleanupCts = new();
|
||||
private readonly Task? _cleanupTask;
|
||||
private Task? _cleanupTask;
|
||||
|
||||
private readonly int _checkEveryFrames = 20;
|
||||
private int _checkEveryFrames = 20;
|
||||
private int _frameCounter = 0;
|
||||
private const int _maxLookupsPerFrame = 30;
|
||||
private const int _maxQueueSize = 100;
|
||||
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 LightFinderScannerService(ILogger<LightFinderScannerService> logger,
|
||||
public BroadcastScannerService(ILogger<BroadcastScannerService> logger,
|
||||
IClientState clientState,
|
||||
IObjectTable objectTable,
|
||||
IFramework framework,
|
||||
LightFinderService broadcastService,
|
||||
BroadcastService broadcastService,
|
||||
LightlessMediator mediator,
|
||||
LightFinderPlateHandler lightFinderPlateHandler,
|
||||
ActorObjectService actorTracker) : base(logger, mediator)
|
||||
NameplateHandler nameplateHandler,
|
||||
DalamudUtilService dalamudUtil,
|
||||
LightlessConfigService configService) : base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_actorTracker = actorTracker;
|
||||
_objectTable = objectTable;
|
||||
_broadcastService = broadcastService;
|
||||
_lightFinderPlateHandler = lightFinderPlateHandler;
|
||||
_nameplateHandler = nameplateHandler;
|
||||
|
||||
_logger = logger;
|
||||
_framework = framework;
|
||||
_framework.Update += OnFrameworkUpdate;
|
||||
|
||||
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
||||
_cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop, _cleanupCts.Token);
|
||||
_cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop);
|
||||
|
||||
_actorTracker = actorTracker;
|
||||
_nameplateHandler.Init();
|
||||
}
|
||||
|
||||
private void OnFrameworkUpdate(IFramework framework) => Update();
|
||||
@@ -64,34 +69,34 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
public void Update()
|
||||
{
|
||||
_frameCounter++;
|
||||
var lookupsThisFrame = 0;
|
||||
_lookupsThisFrame = 0;
|
||||
|
||||
if (!_broadcastService.IsBroadcasting)
|
||||
return;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
foreach (var address in _actorTracker.PlayerAddresses)
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
if (obj is not IPlayerCharacter player || player.Address == IntPtr.Zero)
|
||||
continue;
|
||||
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address);
|
||||
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)
|
||||
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)
|
||||
while (_lookupQueue.Count > 0 && _lookupsThisFrame < MaxLookupsPerFrame)
|
||||
{
|
||||
var cid = _lookupQueue.Dequeue();
|
||||
_lookupQueuedCids.Remove(cid);
|
||||
cidsToLookup.Add(cid);
|
||||
lookupsThisFrame++;
|
||||
_lookupsThisFrame++;
|
||||
}
|
||||
|
||||
if (cidsToLookup.Count > 0 && !_batchRunning)
|
||||
@@ -113,8 +118,8 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
continue;
|
||||
|
||||
var ttl = info.IsBroadcasting && info.TTL.HasValue
|
||||
? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, _maxAllowedTtl.Ticks))
|
||||
: _retryDelay;
|
||||
? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, MaxAllowedTtl.Ticks))
|
||||
: RetryDelay;
|
||||
|
||||
var expiry = now + ttl;
|
||||
|
||||
@@ -128,7 +133,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
.Select(e => e.Key)
|
||||
.ToList();
|
||||
|
||||
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
||||
_nameplateHandler.UpdateBroadcastingCids(activeCids);
|
||||
UpdateSyncshellBroadcasts();
|
||||
}
|
||||
|
||||
@@ -141,7 +146,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
_lookupQueuedCids.Clear();
|
||||
_syncshellCids.Clear();
|
||||
|
||||
_lightFinderPlateHandler.UpdateBroadcastingCids([]);
|
||||
_nameplateHandler.UpdateBroadcastingCids(Enumerable.Empty<string>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +156,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
var newSet = _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Select(e => e.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
.ToHashSet();
|
||||
|
||||
if (!_syncshellCids.SetEquals(newSet))
|
||||
{
|
||||
@@ -167,7 +172,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
return [.. _broadcastCache
|
||||
return _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Select(e => new BroadcastStatusInfoDto
|
||||
{
|
||||
@@ -175,7 +180,8 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
IsBroadcasting = true,
|
||||
TTL = e.Value.ExpiryTime - now,
|
||||
GID = e.Value.GID
|
||||
})];
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task ExpiredBroadcastCleanupLoop()
|
||||
@@ -186,7 +192,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false);
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), token);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var (cid, entry) in _broadcastCache.ToArray())
|
||||
@@ -196,10 +202,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// No action needed when cancelled
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Broadcast cleanup loop crashed");
|
||||
@@ -208,39 +211,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
UpdateSyncshellBroadcasts();
|
||||
}
|
||||
|
||||
public int CountActiveBroadcasts(string? excludeHashedCid = null)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var comparer = StringComparer.Ordinal;
|
||||
return _broadcastCache.Count(entry =>
|
||||
entry.Value.IsBroadcasting &&
|
||||
entry.Value.ExpiryTime > now &&
|
||||
(excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)));
|
||||
}
|
||||
|
||||
public List<KeyValuePair<string, BroadcastEntry>> GetActiveBroadcasts(string? excludeHashedCid = null)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var comparer = StringComparer.Ordinal;
|
||||
return [.. _broadcastCache.Where(entry =>
|
||||
entry.Value.IsBroadcasting &&
|
||||
entry.Value.ExpiryTime > now &&
|
||||
(excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)))];
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
_framework.Update -= OnFrameworkUpdate;
|
||||
if (_cleanupTask != null)
|
||||
{
|
||||
_cleanupTask?.Wait(100, _cleanupCts.Token);
|
||||
}
|
||||
|
||||
_cleanupCts.Cancel();
|
||||
_cleanupCts.Dispose();
|
||||
|
||||
_cleanupTask?.Wait(100);
|
||||
_cleanupCts.Dispose();
|
||||
_nameplateHandler.Uninit();
|
||||
}
|
||||
}
|
||||
377
LightlessSync/Services/BroadcastService.cs
Normal file
377
LightlessSync/Services/BroadcastService.cs
Normal file
@@ -0,0 +1,377 @@
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
private readonly ILogger<BroadcastService> _logger;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly LightlessConfigService _config;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
public bool IsLightFinderAvailable { get; private set; } = true;
|
||||
|
||||
public bool IsBroadcasting => _config.Current.BroadcastEnabled;
|
||||
private bool _syncedOnStartup = false;
|
||||
private bool _waitingForTtlFetch = false;
|
||||
private TimeSpan? _remainingTtl = null;
|
||||
private DateTime _lastTtlCheck = DateTime.MinValue;
|
||||
private DateTime _lastForcedDisableTime = DateTime.MinValue;
|
||||
private static readonly TimeSpan _disableCooldown = TimeSpan.FromSeconds(5);
|
||||
public TimeSpan? RemainingTtl => _remainingTtl;
|
||||
public TimeSpan? RemainingCooldown
|
||||
{
|
||||
get
|
||||
{
|
||||
var elapsed = DateTime.UtcNow - _lastForcedDisableTime;
|
||||
if (elapsed >= _disableCooldown) return null;
|
||||
return _disableCooldown - elapsed;
|
||||
}
|
||||
}
|
||||
|
||||
public BroadcastService(ILogger<BroadcastService> logger, LightlessMediator mediator, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
_config = config;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_apiController = apiController;
|
||||
}
|
||||
|
||||
private async Task RequireConnectionAsync(string context, Func<Task> action)
|
||||
{
|
||||
if (!_apiController.IsConnected)
|
||||
{
|
||||
_logger.LogDebug(context + " skipped, not connected");
|
||||
return;
|
||||
}
|
||||
await action().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
|
||||
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
||||
|
||||
_apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken);
|
||||
//_ = CheckLightfinderSupportAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_mediator.UnsubscribeAll(this);
|
||||
_apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// need to rework this, this is cooked
|
||||
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!_apiController.IsConnected && !cancellationToken.IsCancellationRequested)
|
||||
await Task.Delay(250, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var dummy = "0".PadLeft(64, '0');
|
||||
|
||||
await _apiController.IsUserBroadcasting(dummy).ConfigureAwait(false);
|
||||
await _apiController.SetBroadcastStatus(dummy, true, null).ConfigureAwait(false);
|
||||
await _apiController.GetBroadcastTtl(dummy).ConfigureAwait(false);
|
||||
await _apiController.AreUsersBroadcasting([dummy]).ConfigureAwait(false);
|
||||
|
||||
IsLightFinderAvailable = true;
|
||||
_logger.LogInformation("Lightfinder is available.");
|
||||
}
|
||||
catch (HubException ex) when (ex.Message.Contains("Method does not exist"))
|
||||
{
|
||||
_logger.LogWarning("Lightfinder unavailable: required method missing.");
|
||||
IsLightFinderAvailable = false;
|
||||
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Lightfinder check was canceled.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Lightfinder check failed.");
|
||||
IsLightFinderAvailable = false;
|
||||
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnableBroadcast(EnableBroadcastMessage msg)
|
||||
{
|
||||
_ = RequireConnectionAsync(nameof(OnEnableBroadcast), async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
GroupBroadcastRequestDto? groupDto = null;
|
||||
if (_config.Current.SyncshellFinderEnabled && _config.Current.SelectedFinderSyncshell != null)
|
||||
{
|
||||
groupDto = new GroupBroadcastRequestDto
|
||||
{
|
||||
HashedCID = msg.HashedCid,
|
||||
GID = _config.Current.SelectedFinderSyncshell,
|
||||
Enabled = msg.Enabled,
|
||||
};
|
||||
}
|
||||
|
||||
await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
|
||||
|
||||
if (!msg.Enabled)
|
||||
{
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
return;
|
||||
}
|
||||
|
||||
_waitingForTtlFetch = true;
|
||||
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
|
||||
|
||||
if (ttl is { } remaining && remaining > TimeSpan.Zero)
|
||||
{
|
||||
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
|
||||
_config.Current.BroadcastEnabled = true;
|
||||
_config.Save();
|
||||
|
||||
_logger.LogDebug("Fetched TTL from server: {TTL}", remaining);
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, remaining));
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
|
||||
_waitingForTtlFetch = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to toggle broadcast for {Cid}", msg.HashedCid);
|
||||
_waitingForTtlFetch = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
||||
{
|
||||
_config.Current.BroadcastEnabled = msg.Enabled;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
public async Task<bool> CheckIfBroadcastingAsync(string targetCid)
|
||||
{
|
||||
bool result = false;
|
||||
await RequireConnectionAsync(nameof(CheckIfBroadcastingAsync), async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("[BroadcastCheck] Checking CID: {cid}", targetCid);
|
||||
|
||||
var info = await _apiController.IsUserBroadcasting(targetCid).ConfigureAwait(false);
|
||||
result = info?.TTL > TimeSpan.Zero;
|
||||
|
||||
|
||||
_logger.LogDebug("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to check broadcast status for {cid}", targetCid);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<TimeSpan?> GetBroadcastTtlAsync(string cid)
|
||||
{
|
||||
TimeSpan? ttl = null;
|
||||
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
|
||||
try
|
||||
{
|
||||
ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
return ttl;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, BroadcastStatusInfoDto?>> AreUsersBroadcastingAsync(List<string> hashedCids)
|
||||
{
|
||||
Dictionary<string, BroadcastStatusInfoDto?> result = new();
|
||||
|
||||
await RequireConnectionAsync(nameof(AreUsersBroadcastingAsync), async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var batch = await _apiController.AreUsersBroadcasting(hashedCids).ConfigureAwait(false);
|
||||
|
||||
if (batch?.Results != null)
|
||||
{
|
||||
foreach (var kv in batch.Results)
|
||||
result[kv.Key] = kv.Value;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to batch check broadcast status");
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async void ToggleBroadcast()
|
||||
{
|
||||
if (!IsLightFinderAvailable)
|
||||
{
|
||||
_logger.LogWarning("ToggleBroadcast - Lightfinder is not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
await RequireConnectionAsync(nameof(ToggleBroadcast), async () =>
|
||||
{
|
||||
var cooldown = RemainingCooldown;
|
||||
if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds);
|
||||
return;
|
||||
}
|
||||
|
||||
var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||
|
||||
try
|
||||
{
|
||||
var isCurrentlyBroadcasting = await CheckIfBroadcastingAsync(hashedCid).ConfigureAwait(false);
|
||||
var newStatus = !isCurrentlyBroadcasting;
|
||||
|
||||
if (!newStatus)
|
||||
{
|
||||
_lastForcedDisableTime = DateTime.UtcNow;
|
||||
_logger.LogDebug("Manual disable: cooldown timer started.");
|
||||
}
|
||||
|
||||
_logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
|
||||
|
||||
_mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to determine current broadcast status for toggle");
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnTick(PriorityFrameworkUpdateMessage _)
|
||||
{
|
||||
if (!IsLightFinderAvailable)
|
||||
return;
|
||||
|
||||
if (_config?.Current == null)
|
||||
return;
|
||||
|
||||
if ((DateTime.UtcNow - _lastTtlCheck).TotalSeconds < 1)
|
||||
return;
|
||||
|
||||
_lastTtlCheck = DateTime.UtcNow;
|
||||
|
||||
await RequireConnectionAsync(nameof(OnTick), async () => {
|
||||
if (!_syncedOnStartup && _config.Current.BroadcastEnabled)
|
||||
{
|
||||
_syncedOnStartup = true;
|
||||
try
|
||||
{
|
||||
string hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||
if (ttl is { }
|
||||
remaining && remaining > TimeSpan.Zero)
|
||||
{
|
||||
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
|
||||
_config.Current.BroadcastEnabled = true;
|
||||
_config.Save();
|
||||
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh TTL in OnTick");
|
||||
}
|
||||
}
|
||||
if (_config.Current.BroadcastEnabled)
|
||||
{
|
||||
if (_waitingForTtlFetch)
|
||||
{
|
||||
_logger.LogDebug("OnTick skipped: waiting for TTL fetch");
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime expiry = _config.Current.BroadcastTtl;
|
||||
TimeSpan remaining = expiry - DateTime.UtcNow;
|
||||
_remainingTtl = remaining > TimeSpan.Zero ? remaining : null;
|
||||
if (_remainingTtl == null)
|
||||
{
|
||||
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_remainingTtl = null;
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@ using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.CharaData.Models;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -28,7 +28,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
||||
private readonly List<CharaDataMetaInfoExtendedDto> _nearbyData = [];
|
||||
private readonly CharaDataNearbyManager _nearbyManager;
|
||||
private readonly CharaDataCharacterHandler _characterHandler;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly Dictionary<string, CharaDataFullExtendedDto> _ownCharaData = [];
|
||||
private readonly Dictionary<string, Task> _sharedMetaInfoTimeoutTasks = [];
|
||||
private readonly Dictionary<UserData, List<CharaDataMetaInfoExtendedDto>> _sharedWithYouData = [];
|
||||
@@ -45,7 +45,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
||||
LightlessMediator lightlessMediator, IpcManager ipcManager, DalamudUtilService dalamudUtilService,
|
||||
FileDownloadManagerFactory fileDownloadManagerFactory,
|
||||
CharaDataConfigService charaDataConfigService, CharaDataNearbyManager charaDataNearbyManager,
|
||||
CharaDataCharacterHandler charaDataCharacterHandler, PairUiService pairUiService) : base(logger, lightlessMediator)
|
||||
CharaDataCharacterHandler charaDataCharacterHandler, PairManager pairManager) : base(logger, lightlessMediator)
|
||||
{
|
||||
_apiController = apiController;
|
||||
_fileHandler = charaDataFileHandler;
|
||||
@@ -54,7 +54,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
||||
_configService = charaDataConfigService;
|
||||
_nearbyManager = charaDataNearbyManager;
|
||||
_characterHandler = charaDataCharacterHandler;
|
||||
_pairUiService = pairUiService;
|
||||
_pairManager = pairManager;
|
||||
lightlessMediator.Subscribe<ConnectedMessage>(this, (msg) =>
|
||||
{
|
||||
_connectCts?.Cancel();
|
||||
@@ -421,10 +421,9 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
||||
});
|
||||
|
||||
var result = await GetSharedWithYouTask.ConfigureAwait(false);
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
foreach (var grouping in result.GroupBy(r => r.Uploader))
|
||||
{
|
||||
snapshot.PairsByUid.TryGetValue(grouping.Key.UID, out var pair);
|
||||
var pair = _pairManager.GetPairByUID(grouping.Key.UID);
|
||||
if (pair?.IsPaused ?? false) continue;
|
||||
List<CharaDataMetaInfoExtendedDto> newList = new();
|
||||
foreach (var item in grouping)
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.Services.CharaData.Models;
|
||||
using System.Collections.Immutable;
|
||||
namespace LightlessSync.Services.CharaData;
|
||||
|
||||
public sealed class CharacterAnalysisSummary
|
||||
{
|
||||
public static CharacterAnalysisSummary Empty { get; } =
|
||||
new(ImmutableDictionary<ObjectKind, CharacterAnalysisObjectSummary>.Empty);
|
||||
|
||||
internal CharacterAnalysisSummary(IImmutableDictionary<ObjectKind, CharacterAnalysisObjectSummary> objects)
|
||||
{
|
||||
Objects = objects;
|
||||
}
|
||||
|
||||
public IImmutableDictionary<ObjectKind, CharacterAnalysisObjectSummary> Objects { get; }
|
||||
|
||||
public bool HasData => Objects.Any(kvp => kvp.Value.HasEntries);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
namespace LightlessSync.Services.CharaData.Models;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly record struct CharacterAnalysisObjectSummary(int EntryCount, long TotalTriangles, long TexOriginalBytes, long TexCompressedBytes)
|
||||
{
|
||||
public bool HasEntries => EntryCount > 0;
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Services.CharaData;
|
||||
using LightlessSync.Services.CharaData.Models;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.Utils;
|
||||
using Lumina.Data.Files;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
@@ -18,7 +16,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
private CancellationTokenSource? _analysisCts;
|
||||
private CancellationTokenSource _baseAnalysisCts = new();
|
||||
private string _lastDataHash = string.Empty;
|
||||
private CharacterAnalysisSummary _latestSummary = CharacterAnalysisSummary.Empty;
|
||||
|
||||
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, LightlessMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
|
||||
: base(logger, mediator)
|
||||
@@ -37,98 +34,71 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
public bool IsAnalysisRunning => _analysisCts != null;
|
||||
public int TotalFiles { get; internal set; }
|
||||
internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = [];
|
||||
public CharacterAnalysisSummary LatestSummary => _latestSummary;
|
||||
|
||||
public void CancelAnalyze()
|
||||
{
|
||||
_analysisCts?.CancelDispose();
|
||||
_analysisCts = null;
|
||||
}
|
||||
|
||||
public async Task ComputeAnalysis(bool print = true, bool recalculate = false)
|
||||
{
|
||||
Logger.LogDebug("=== Calculating Character Analysis ===");
|
||||
|
||||
_analysisCts = _analysisCts?.CancelRecreate() ?? new();
|
||||
|
||||
var cancelToken = _analysisCts.Token;
|
||||
|
||||
var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList();
|
||||
|
||||
var remaining = allFiles.Where(c => !c.IsComputed || recalculate).ToList();
|
||||
|
||||
if (remaining.Count == 0)
|
||||
return;
|
||||
|
||||
TotalFiles = remaining.Count;
|
||||
CurrentFile = 0;
|
||||
|
||||
Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count);
|
||||
|
||||
Mediator.Publish(new HaltScanMessage(nameof(CharacterAnalyzer)));
|
||||
|
||||
try
|
||||
if (allFiles.Exists(c => !c.IsComputed || recalculate))
|
||||
{
|
||||
foreach (var file in remaining)
|
||||
var remaining = allFiles.Where(c => !c.IsComputed || recalculate).ToList();
|
||||
TotalFiles = remaining.Count;
|
||||
CurrentFile = 1;
|
||||
Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count);
|
||||
|
||||
Mediator.Publish(new HaltScanMessage(nameof(CharacterAnalyzer)));
|
||||
try
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
foreach (var file in remaining)
|
||||
{
|
||||
Logger.LogDebug("Computing file {file}", file.FilePaths[0]);
|
||||
await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false);
|
||||
CurrentFile++;
|
||||
}
|
||||
|
||||
var path = file.FilePaths.FirstOrDefault() ?? "<unknown>";
|
||||
Logger.LogDebug("Computing file {file}", path);
|
||||
_fileCacheManager.WriteOutFullCsv();
|
||||
|
||||
await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false);
|
||||
|
||||
CurrentFile++;
|
||||
}
|
||||
|
||||
await _fileCacheManager.WriteOutFullCsvAsync(cancelToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogInformation("File analysis cancelled");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to analyze files");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(CharacterAnalyzer)));
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed to analyze files");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(CharacterAnalyzer)));
|
||||
}
|
||||
}
|
||||
|
||||
RecalculateSummary();
|
||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||
|
||||
_analysisCts.CancelDispose();
|
||||
_analysisCts = null;
|
||||
|
||||
if (print) PrintAnalysis();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_analysisCts.CancelDispose();
|
||||
_baseAnalysisCts.Dispose();
|
||||
}
|
||||
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token)
|
||||
{
|
||||
var normalized = new HashSet<string>(
|
||||
filePaths.Where(path => !string.IsNullOrWhiteSpace(path)),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
if (normalized.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var objectEntries in LastAnalysis.Values)
|
||||
{
|
||||
foreach (var entry in objectEntries.Values)
|
||||
{
|
||||
if (!entry.FilePaths.Exists(path => normalized.Contains(path)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
await entry.ComputeSizes(_fileCacheManager, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
||||
{
|
||||
if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return;
|
||||
|
||||
LastAnalysis.Clear();
|
||||
|
||||
foreach (var obj in charaData.FileReplacements)
|
||||
{
|
||||
Dictionary<string, FileDataEntry> data = new(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -136,8 +106,9 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var fileCacheEntries = (await _fileCacheManager.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false)).ToList();
|
||||
var fileCacheEntries = _fileCacheManager.GetAllFileCachesByHash(fileEntry.Hash, ignoreCacheEntries: true, validate: false).ToList();
|
||||
if (fileCacheEntries.Count == 0) continue;
|
||||
|
||||
var filePath = fileCacheEntries[0].ResolvedFilepath;
|
||||
FileInfo fi = new(filePath);
|
||||
string ext = "unk?";
|
||||
@@ -149,49 +120,28 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
{
|
||||
Logger.LogWarning(ex, "Could not identify extension for {path}", filePath);
|
||||
}
|
||||
|
||||
var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false);
|
||||
|
||||
foreach (var entry in fileCacheEntries)
|
||||
{
|
||||
data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext,
|
||||
[.. fileEntry.GamePaths],
|
||||
[.. fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal)],
|
||||
fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct().ToList(),
|
||||
entry.Size > 0 ? entry.Size.Value : 0,
|
||||
entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0,
|
||||
tris);
|
||||
}
|
||||
}
|
||||
|
||||
LastAnalysis[obj.Key] = data;
|
||||
}
|
||||
|
||||
RecalculateSummary();
|
||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||
|
||||
_lastDataHash = charaData.DataHash.Value;
|
||||
}
|
||||
private void RecalculateSummary()
|
||||
{
|
||||
var builder = ImmutableDictionary.CreateBuilder<ObjectKind, CharacterAnalysisObjectSummary>();
|
||||
|
||||
foreach (var (objectKind, entries) in LastAnalysis)
|
||||
{
|
||||
long totalTriangles = 0;
|
||||
long texOriginalBytes = 0;
|
||||
long texCompressedBytes = 0;
|
||||
|
||||
foreach (var entry in entries.Values)
|
||||
{
|
||||
totalTriangles += entry.Triangles;
|
||||
if (string.Equals(entry.FileType, "tex", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
texOriginalBytes += entry.OriginalSize;
|
||||
texCompressedBytes += entry.CompressedSize;
|
||||
}
|
||||
}
|
||||
|
||||
builder[objectKind] = new CharacterAnalysisObjectSummary(entries.Count, totalTriangles, texOriginalBytes, texCompressedBytes);
|
||||
}
|
||||
|
||||
_latestSummary = new CharacterAnalysisSummary(builder.ToImmutable());
|
||||
}
|
||||
private void PrintAnalysis()
|
||||
{
|
||||
if (LastAnalysis.Count == 0) return;
|
||||
@@ -200,6 +150,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
int fileCounter = 1;
|
||||
int totalFiles = kvp.Value.Count;
|
||||
Logger.LogInformation("=== Analysis for {obj} ===", kvp.Key);
|
||||
|
||||
foreach (var entry in kvp.Value.OrderBy(b => b.Value.GamePaths.OrderBy(p => p, StringComparer.Ordinal).First(), StringComparer.Ordinal))
|
||||
{
|
||||
Logger.LogInformation("File {x}/{y}: {hash}", fileCounter++, totalFiles, entry.Key);
|
||||
@@ -228,6 +179,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
Logger.LogInformation("Total files: {count}, size extracted: {size}, size compressed: {sizeComp}", kvp.Value.Count,
|
||||
UiSharedService.ByteToString(kvp.Value.Sum(v => v.Value.OriginalSize)), UiSharedService.ByteToString(kvp.Value.Sum(v => v.Value.CompressedSize)));
|
||||
}
|
||||
|
||||
Logger.LogInformation("=== Total summary for all currently present objects ===");
|
||||
Logger.LogInformation("Total files: {count}, size extracted: {size}, size compressed: {sizeComp}",
|
||||
LastAnalysis.Values.Sum(v => v.Values.Count),
|
||||
@@ -235,6 +187,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize))));
|
||||
Logger.LogInformation("IMPORTANT NOTES:\n\r- For Lightless up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
|
||||
}
|
||||
|
||||
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
|
||||
{
|
||||
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
|
||||
@@ -242,7 +195,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
{
|
||||
var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false);
|
||||
var normalSize = new FileInfo(FilePaths[0]).Length;
|
||||
var entries = await fileCacheManager.GetAllFileCachesByHashAsync(Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false);
|
||||
var entries = fileCacheManager.GetAllFileCachesByHash(Hash, ignoreCacheEntries: true, validate: false);
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
entry.Size = normalSize;
|
||||
@@ -250,40 +203,33 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
}
|
||||
OriginalSize = normalSize;
|
||||
CompressedSize = compressedsize.Item2.LongLength;
|
||||
RefreshFormat();
|
||||
}
|
||||
public long OriginalSize { get; private set; } = OriginalSize;
|
||||
public long CompressedSize { get; private set; } = CompressedSize;
|
||||
public long Triangles { get; private set; } = Triangles;
|
||||
public Lazy<string> Format => _format ??= CreateFormatValue();
|
||||
|
||||
private Lazy<string>? _format;
|
||||
|
||||
public void RefreshFormat()
|
||||
public Lazy<string> Format = new(() =>
|
||||
{
|
||||
_format = CreateFormatValue();
|
||||
}
|
||||
|
||||
private Lazy<string> CreateFormatValue()
|
||||
=> new(() =>
|
||||
switch (FileType)
|
||||
{
|
||||
if (!string.Equals(FileType, "tex", StringComparison.Ordinal))
|
||||
{
|
||||
case "tex":
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(FilePaths[0], FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
using var reader = new BinaryReader(stream);
|
||||
reader.BaseStream.Position = 4;
|
||||
var format = (TexFile.TextureFormat)reader.ReadInt32();
|
||||
return format.ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(FilePaths[0], FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
using var reader = new BinaryReader(stream);
|
||||
reader.BaseStream.Position = 4;
|
||||
var format = (TexFile.TextureFormat)reader.ReadInt32();
|
||||
return format.ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
|
||||
namespace LightlessSync.Services.Chat;
|
||||
|
||||
public sealed record ChatMessageEntry(
|
||||
ChatMessageDto? Payload,
|
||||
string DisplayName,
|
||||
bool FromSelf,
|
||||
DateTime ReceivedAtUtc,
|
||||
ChatSystemEntry? SystemMessage = null)
|
||||
{
|
||||
public bool IsSystem => SystemMessage is not null;
|
||||
}
|
||||
|
||||
public enum ChatSystemEntryType
|
||||
{
|
||||
ZoneSeparator
|
||||
}
|
||||
|
||||
public sealed record ChatSystemEntry(ChatSystemEntryType Type, string? ZoneName);
|
||||
|
||||
public readonly record struct ChatChannelSnapshot(
|
||||
string Key,
|
||||
ChatChannelDescriptor Descriptor,
|
||||
string DisplayName,
|
||||
ChatChannelType Type,
|
||||
bool IsConnected,
|
||||
bool IsAvailable,
|
||||
string? StatusText,
|
||||
bool HasUnread,
|
||||
int UnreadCount,
|
||||
IReadOnlyList<ChatMessageEntry> Messages);
|
||||
|
||||
public readonly record struct ChatReportResult(bool Success, string? ErrorMessage);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,8 +13,7 @@ namespace LightlessSync.Services;
|
||||
|
||||
public sealed class CommandManagerService : IDisposable
|
||||
{
|
||||
private const string _longName = "/lightless";
|
||||
private const string _shortName = "/light";
|
||||
private const string _commandName = "/light";
|
||||
|
||||
private readonly ApiController _apiController;
|
||||
private readonly ICommandManager _commandManager;
|
||||
@@ -35,11 +34,7 @@ public sealed class CommandManagerService : IDisposable
|
||||
_apiController = apiController;
|
||||
_mediator = mediator;
|
||||
_lightlessConfigService = lightlessConfigService;
|
||||
_commandManager.AddHandler(_longName, new CommandInfo(OnCommand)
|
||||
{
|
||||
HelpMessage = $"\u2191;"
|
||||
});
|
||||
_commandManager.AddHandler(_shortName, new CommandInfo(OnCommand)
|
||||
_commandManager.AddHandler(_commandName, new CommandInfo(OnCommand)
|
||||
{
|
||||
HelpMessage = "Opens the Lightless Sync UI" + Environment.NewLine + Environment.NewLine +
|
||||
"Additionally possible commands:" + Environment.NewLine +
|
||||
@@ -48,14 +43,13 @@ public sealed class CommandManagerService : IDisposable
|
||||
"\t /light gpose - Opens the Lightless Character Data Hub window" + Environment.NewLine +
|
||||
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
||||
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
||||
"\t /light finder - Opens the Lightfinder window"
|
||||
"\t /light lightfinder - Opens the Lightfinder window"
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_commandManager.RemoveHandler(_longName);
|
||||
_commandManager.RemoveHandler(_shortName);
|
||||
_commandManager.RemoveHandler(_commandName);
|
||||
}
|
||||
|
||||
private void OnCommand(string command, string args)
|
||||
@@ -129,9 +123,9 @@ public sealed class CommandManagerService : IDisposable
|
||||
{
|
||||
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
||||
}
|
||||
else if (string.Equals(splitArgs[0], "finder", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(splitArgs[0], "lightfinder", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_mediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
||||
_mediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace LightlessSync.Services.Compactor
|
||||
{
|
||||
/// <summary>
|
||||
/// This batch service is made for the File Frag command, because of each file needing to use this command.
|
||||
/// It's better to combine into one big command in batches then doing each command on each compressed call.
|
||||
/// </summary>
|
||||
public sealed partial class BatchFilefragService : IDisposable
|
||||
{
|
||||
private readonly Channel<(string path, TaskCompletionSource<bool> tcs)> _ch;
|
||||
private readonly Task _worker;
|
||||
private readonly bool _useShell;
|
||||
private readonly ILogger _log;
|
||||
private readonly int _batchSize;
|
||||
private readonly TimeSpan _flushDelay;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
public delegate (bool ok, string stdout, string stderr, int exitCode) RunDirect(string fileName, IEnumerable<string> args, string? workingDir, int timeoutMs);
|
||||
private readonly RunDirect _runDirect;
|
||||
|
||||
public delegate (bool ok, string stdout, string stderr, int exitCode) RunShell(string command, string? workingDir, int timeoutMs);
|
||||
private readonly RunShell _runShell;
|
||||
|
||||
public BatchFilefragService(bool useShell, ILogger log, int batchSize = 128, int flushMs = 25, RunDirect? runDirect = null, RunShell? runShell = null)
|
||||
{
|
||||
_useShell = useShell;
|
||||
_log = log;
|
||||
_batchSize = Math.Max(8, batchSize);
|
||||
_flushDelay = TimeSpan.FromMilliseconds(Math.Max(5, flushMs));
|
||||
_ch = Channel.CreateUnbounded<(string, TaskCompletionSource<bool>)>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });
|
||||
|
||||
// require runners to be setup, wouldnt start otherwise
|
||||
if (runDirect is null || runShell is null)
|
||||
throw new ArgumentNullException(nameof(runDirect), "Provide process runners from FileCompactor");
|
||||
_runDirect = runDirect;
|
||||
_runShell = runShell;
|
||||
|
||||
_worker = Task.Run(ProcessAsync, _cts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the file is compressed using Btrfs using tasks
|
||||
/// </summary>
|
||||
/// <param name="linuxPath">Linux/Wine path given for the file.</param>
|
||||
/// <param name="ct">Cancellation Token</param>
|
||||
/// <returns>If it was compressed or not</returns>
|
||||
public Task<bool> IsCompressedAsync(string linuxPath, CancellationToken ct = default)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
if (!_ch.Writer.TryWrite((linuxPath, tcs)))
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
if (ct.CanBeCanceled)
|
||||
{
|
||||
var reg = ct.Register(() => tcs.TrySetCanceled(ct));
|
||||
_ = tcs.Task.ContinueWith(_ => reg.Dispose(), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the pending compression tasks asynchronously
|
||||
/// </summary>
|
||||
/// <returns>Task</returns>
|
||||
private async Task ProcessAsync()
|
||||
{
|
||||
var reader = _ch.Reader;
|
||||
var pending = new List<(string path, TaskCompletionSource<bool> tcs)>(_batchSize);
|
||||
|
||||
try
|
||||
{
|
||||
while (await reader.WaitToReadAsync(_cts.Token).ConfigureAwait(false))
|
||||
{
|
||||
if (!reader.TryRead(out var first)) continue;
|
||||
pending.Add(first);
|
||||
|
||||
var flushAt = DateTime.UtcNow + _flushDelay;
|
||||
while (pending.Count < _batchSize && DateTime.UtcNow < flushAt)
|
||||
{
|
||||
if (reader.TryRead(out var item))
|
||||
{
|
||||
pending.Add(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((flushAt - DateTime.UtcNow) <= TimeSpan.Zero) break;
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(5), _cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var map = RunBatch(pending.Select(p => p.path));
|
||||
foreach (var (path, tcs) in pending)
|
||||
{
|
||||
tcs.TrySetResult(map.TryGetValue(path, out var c) && c);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogDebug(ex, "filefrag batch failed. falling back to false");
|
||||
foreach (var (_, tcs) in pending)
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
pending.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
//Shutting down worker, exception called
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Running the batch of each file in the queue in one file frag command.
|
||||
/// </summary>
|
||||
/// <param name="paths">Paths that are needed for the command building for the batch return</param>
|
||||
/// <returns>Path of the file and if it went correctly</returns>
|
||||
/// <exception cref="InvalidOperationException">Failing to start filefrag on the system if this exception is found</exception>
|
||||
private Dictionary<string, bool> RunBatch(IEnumerable<string> paths)
|
||||
{
|
||||
var list = paths.Distinct(StringComparer.Ordinal).ToList();
|
||||
var result = list.ToDictionary(p => p, _ => false, StringComparer.Ordinal);
|
||||
|
||||
(bool ok, string stdout, string stderr, int code) res;
|
||||
|
||||
if (_useShell)
|
||||
{
|
||||
var inner = "filefrag -v -- " + string.Join(' ', list.Select(QuoteSingle));
|
||||
res = _runShell(inner, timeoutMs: 15000, workingDir: "/");
|
||||
}
|
||||
else
|
||||
{
|
||||
var args = new List<string> { "-v", "--" };
|
||||
args.AddRange(list);
|
||||
res = _runDirect("filefrag", args, workingDir: "/", timeoutMs: 15000);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(res.stderr))
|
||||
_log.LogTrace("filefrag stderr (batch): {err}", res.stderr.Trim());
|
||||
|
||||
ParseFilefrag(res.stdout, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsing the string given from the File Frag command into mapping
|
||||
/// </summary>
|
||||
/// <param name="output">Output of the process from the File Frag</param>
|
||||
/// <param name="map">Mapping of the processed files</param>
|
||||
private static void ParseFilefrag(string output, Dictionary<string, bool> map)
|
||||
{
|
||||
var reHeaderColon = ColonRegex();
|
||||
var reHeaderSize = SizeRegex();
|
||||
|
||||
string? current = null;
|
||||
using var sr = new StringReader(output);
|
||||
for (string? line = sr.ReadLine(); line != null; line = sr.ReadLine())
|
||||
{
|
||||
var m1 = reHeaderColon.Match(line);
|
||||
if (m1.Success) { current = m1.Groups[1].Value; continue; }
|
||||
|
||||
var m2 = reHeaderSize.Match(line);
|
||||
if (m2.Success) { current = m2.Groups[1].Value; continue; }
|
||||
|
||||
if (current is not null && line.Contains("flags:", StringComparison.OrdinalIgnoreCase) &&
|
||||
line.Contains("compressed", StringComparison.OrdinalIgnoreCase) && map.ContainsKey(current))
|
||||
{
|
||||
map[current] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string QuoteSingle(string s) => "'" + s.Replace("'", "'\\''", StringComparison.Ordinal) + "'";
|
||||
|
||||
/// <summary>
|
||||
/// Regex of the File Size return on the Linux/Wine systems, giving back the amount
|
||||
/// </summary>
|
||||
/// <returns>Regex of the File Size</returns>
|
||||
[GeneratedRegex(@"^File size of (/.+?) is ", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 500)]
|
||||
private static partial Regex SizeRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Regex on colons return on the Linux/Wine systems
|
||||
/// </summary>
|
||||
/// <returns>Regex of the colons in the given path</returns>
|
||||
[GeneratedRegex(@"^(/.+?):\s", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 500)]
|
||||
private static partial Regex ColonRegex();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_ch.Writer.TryComplete();
|
||||
_cts.Cancel();
|
||||
try
|
||||
{
|
||||
_worker.Wait(TimeSpan.FromSeconds(2), _cts.Token);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore the catch in dispose
|
||||
}
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Lumina.Excel.Sheets;
|
||||
using LightlessSync.UI.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.Services.LightFinder;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
@@ -24,17 +20,11 @@ internal class ContextMenuService : IHostedService
|
||||
private readonly ILogger<ContextMenuService> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairRequestService _pairRequestService;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly LightFinderScannerService _broadcastScannerService;
|
||||
private readonly LightFinderService _broadcastService;
|
||||
private readonly LightlessProfileManager _lightlessProfileManager;
|
||||
private readonly LightlessMediator _mediator;
|
||||
|
||||
private const int _lightlessPrefixColor = 708;
|
||||
|
||||
public ContextMenuService(
|
||||
IContextMenu contextMenu,
|
||||
@@ -43,15 +33,11 @@ internal class ContextMenuService : IHostedService
|
||||
ILogger<ContextMenuService> logger,
|
||||
DalamudUtilService dalamudUtil,
|
||||
ApiController apiController,
|
||||
IObjectTable objectTable,
|
||||
IObjectTable objectTable,
|
||||
LightlessConfigService configService,
|
||||
PairRequestService pairRequestService,
|
||||
PairUiService pairUiService,
|
||||
IClientState clientState,
|
||||
LightFinderScannerService broadcastScannerService,
|
||||
LightFinderService broadcastService,
|
||||
LightlessProfileManager lightlessProfileManager,
|
||||
LightlessMediator mediator)
|
||||
PairManager pairManager,
|
||||
IClientState clientState)
|
||||
{
|
||||
_contextMenu = contextMenu;
|
||||
_pluginInterface = pluginInterface;
|
||||
@@ -61,13 +47,9 @@ internal class ContextMenuService : IHostedService
|
||||
_apiController = apiController;
|
||||
_objectTable = objectTable;
|
||||
_configService = configService;
|
||||
_pairUiService = pairUiService;
|
||||
_pairManager = pairManager;
|
||||
_pairRequestService = pairRequestService;
|
||||
_clientState = clientState;
|
||||
_broadcastScannerService = broadcastScannerService;
|
||||
_broadcastService = broadcastService;
|
||||
_lightlessProfileManager = lightlessProfileManager;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
@@ -96,109 +78,52 @@ internal class ContextMenuService : IHostedService
|
||||
|
||||
private void OnMenuOpened(IMenuOpenedArgs args)
|
||||
{
|
||||
|
||||
if (!_pluginInterface.UiBuilder.ShouldModifyUi)
|
||||
return;
|
||||
|
||||
if (args.AddonName != null)
|
||||
{
|
||||
var addonName = args.AddonName;
|
||||
_logger.LogTrace("Context menu addon name: {AddonName}", addonName);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//Check if target is not menutargetdefault.
|
||||
if (args.Target is not MenuTargetDefault target)
|
||||
{
|
||||
_logger.LogTrace("Context menu target is not MenuTargetDefault.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Context menu opened for target: {Target}", target.TargetName ?? "null");
|
||||
|
||||
//Check if name or target id isnt null/zero
|
||||
if (string.IsNullOrEmpty(target.TargetName) || target.TargetObjectId == 0 || target.TargetHomeWorld.RowId == 0)
|
||||
{
|
||||
_logger.LogTrace("Context menu target has invalid data: Name='{TargetName}', ObjectId={TargetObjectId}, HomeWorldId={TargetHomeWorldId}", target.TargetName, target.TargetObjectId, target.TargetHomeWorld.RowId);
|
||||
return;
|
||||
}
|
||||
|
||||
//Check if it is a real target.
|
||||
IPlayerCharacter? targetData = GetPlayerFromObjectTable(target);
|
||||
if (targetData == null || targetData.Address == nint.Zero || _objectTable.LocalPlayer == null)
|
||||
{
|
||||
_logger.LogTrace("Target player {TargetName}@{World} not found in object table.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||
if (targetData == null || targetData.Address == nint.Zero)
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
|
||||
p.IsVisible &&
|
||||
p.PlayerCharacterId != uint.MaxValue &&
|
||||
p.PlayerCharacterId == target.TargetObjectId);
|
||||
|
||||
if (pair is not null)
|
||||
{
|
||||
_logger.LogTrace("Target player {TargetName}@{World} is already paired, adding existing pair context menu.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||
|
||||
pair.AddContextMenu(args);
|
||||
if (!pair.IsDirectlyPaired)
|
||||
{
|
||||
_logger.LogTrace("Target player {TargetName}@{World} is not directly paired, add direct pair menu item", target.TargetName, target.TargetHomeWorld.RowId);
|
||||
AddDirectPairMenuItem(args);
|
||||
}
|
||||
|
||||
//Check if user is paired or is own.
|
||||
if (VisibleUserIds.Any(u => u == target.TargetObjectId) || _clientState.LocalPlayer.GameObjectId == target.TargetObjectId)
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Target player {TargetName}@{World} is not paired, adding direct pair request context menu.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||
|
||||
//Check if user is directly paired or is own.
|
||||
if (VisibleUserIds.Any(u => u == target.TargetObjectId) || _objectTable.LocalPlayer?.GameObjectId == target.TargetObjectId || !_configService.Current.EnableRightClickMenus)
|
||||
{
|
||||
_logger.LogTrace("Target player {TargetName}@{World} is already paired or is self, or right-click menus are disabled.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||
return;
|
||||
}
|
||||
|
||||
//Check if in PVP or GPose
|
||||
if (_clientState.IsPvPExcludingDen || _clientState.IsGPosing)
|
||||
{
|
||||
_logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||
return;
|
||||
}
|
||||
|
||||
//Check for valid world.
|
||||
var world = GetWorld(target.TargetHomeWorld.RowId);
|
||||
if (!IsWorldValid(world))
|
||||
{
|
||||
_logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||
return;
|
||||
}
|
||||
|
||||
string? targetHashedCid = null;
|
||||
if (_broadcastService.IsBroadcasting)
|
||||
if (!_configService.Current.EnableRightClickMenus)
|
||||
return;
|
||||
|
||||
args.AddMenuItem(new MenuItem
|
||||
{
|
||||
targetHashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(targetHashedCid) && CanOpenLightfinderProfile(targetHashedCid))
|
||||
{
|
||||
var hashedCid = targetHashedCid;
|
||||
UiSharedService.AddContextMenuItem(args, name: "Open Lightless Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () => HandleLightfinderProfileSelection(hashedCid));
|
||||
}
|
||||
|
||||
AddDirectPairMenuItem(args);
|
||||
Name = "Send Pair Request",
|
||||
PrefixChar = 'L',
|
||||
UseDefaultPrefix = false,
|
||||
PrefixColor = 708,
|
||||
OnClicked = async _ => await HandleSelection(args).ConfigureAwait(false)
|
||||
});
|
||||
}
|
||||
|
||||
private void AddDirectPairMenuItem(IMenuOpenedArgs args)
|
||||
{
|
||||
UiSharedService.AddContextMenuItem(
|
||||
args,
|
||||
name: "Send Direct Pair Request",
|
||||
prefixChar: 'L',
|
||||
colorMenuItem: _lightlessPrefixColor,
|
||||
onClick: () => HandleSelection(args));
|
||||
}
|
||||
|
||||
private HashSet<ulong> VisibleUserIds =>
|
||||
[.. _pairUiService.GetSnapshot().PairsByUid.Values
|
||||
.Where(p => p.IsVisible && p.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(p => (ulong)p.PlayerCharacterId)];
|
||||
|
||||
private async Task HandleSelection(IMenuArgs args)
|
||||
{
|
||||
if (args.Target is not MenuTargetDefault target)
|
||||
@@ -222,7 +147,7 @@ internal class ContextMenuService : IHostedService
|
||||
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
|
||||
|
||||
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
|
||||
await _apiController.TryPairWithContentId(receiverCid).ConfigureAwait(false);
|
||||
await _apiController.TryPairWithContentId(receiverCid, senderCid).ConfigureAwait(false);
|
||||
if (!string.IsNullOrWhiteSpace(receiverCid))
|
||||
{
|
||||
_pairRequestService.RemoveRequest(receiverCid);
|
||||
@@ -234,48 +159,9 @@ internal class ContextMenuService : IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleLightfinderProfileSelection(string hashedCid)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hashedCid))
|
||||
return;
|
||||
|
||||
if (!_broadcastService.IsBroadcasting)
|
||||
{
|
||||
Notify("Lightfinder inactive", "Enable Lightfinder to open broadcaster profiles.", NotificationType.Warning, 6);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_broadcastScannerService.BroadcastCache.TryGetValue(hashedCid, out var entry) || !entry.IsBroadcasting || entry.ExpiryTime <= DateTime.UtcNow)
|
||||
{
|
||||
Notify("Broadcaster unavailable", "That player is not currently using Lightfinder.", NotificationType.Info, 5);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _lightlessProfileManager.GetLightfinderProfileAsync(hashedCid).ConfigureAwait(false);
|
||||
if (result == null)
|
||||
{
|
||||
Notify("Profile unavailable", "Unable to load Lightless profile for that player.", NotificationType.Error, 6);
|
||||
return;
|
||||
}
|
||||
|
||||
_mediator.Publish(new OpenLightfinderProfileMessage(result.Value.User, result.Value.ProfileData, hashedCid));
|
||||
}
|
||||
|
||||
private void Notify(string title, string message, NotificationType type, double durationSeconds)
|
||||
{
|
||||
_mediator.Publish(new NotificationMessage(title, message, type, TimeSpan.FromSeconds(durationSeconds)));
|
||||
}
|
||||
|
||||
private bool CanOpenLightfinderProfile(string hashedCid)
|
||||
{
|
||||
if (!_broadcastService.IsBroadcasting)
|
||||
return false;
|
||||
|
||||
if (!_broadcastScannerService.BroadcastCache.TryGetValue(hashedCid, out var entry))
|
||||
return false;
|
||||
|
||||
return entry.IsBroadcasting && entry.ExpiryTime > DateTime.UtcNow;
|
||||
}
|
||||
private HashSet<ulong> VisibleUserIds => [.. _pairManager.GetOnlineUserPairs()
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||
|
||||
private IPlayerCharacter? GetPlayerFromObjectTable(MenuTargetDefault target)
|
||||
{
|
||||
@@ -314,6 +200,8 @@ internal class ContextMenuService : IHostedService
|
||||
|
||||
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();
|
||||
|
||||
@@ -12,10 +12,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.Interop;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using Lumina.Excel.Sheets;
|
||||
@@ -24,9 +21,7 @@ using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
@@ -42,27 +37,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly ILogger<DalamudUtilService> _logger;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private readonly ITargetManager _targetManager;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private readonly Lazy<PairFactory> _pairFactory;
|
||||
private PairUniqueIdentifier? _FocusPairIdent;
|
||||
private IGameObject? _FocusOriginalTarget;
|
||||
private uint? _classJobId = 0;
|
||||
private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
||||
private string _lastGlobalBlockPlayer = string.Empty;
|
||||
private string _lastGlobalBlockReason = string.Empty;
|
||||
private ushort _lastZone = 0;
|
||||
private ushort _lastWorldId = 0;
|
||||
private readonly Dictionary<string, (string Name, nint Address)> _playerCharas = new(StringComparer.Ordinal);
|
||||
private readonly List<string> _notUpdatedCharas = [];
|
||||
private bool _sentBetweenAreas = false;
|
||||
private Lazy<ulong> _cid;
|
||||
|
||||
public DalamudUtilService(ILogger<DalamudUtilService> logger, IClientState clientState, IObjectTable objectTable, IFramework framework,
|
||||
IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig,
|
||||
ActorObjectService actorObjectService, BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector,
|
||||
LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService, Lazy<PairFactory> pairFactory)
|
||||
BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector,
|
||||
LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService)
|
||||
{
|
||||
_logger = logger;
|
||||
_clientState = clientState;
|
||||
@@ -72,14 +63,11 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_condition = condition;
|
||||
_gameData = gameData;
|
||||
_gameConfig = gameConfig;
|
||||
_actorObjectService = actorObjectService;
|
||||
_targetManager = targetManager;
|
||||
_blockedCharacterHandler = blockedCharacterHandler;
|
||||
Mediator = mediator;
|
||||
_performanceCollector = performanceCollector;
|
||||
_configService = configService;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
_pairFactory = pairFactory;
|
||||
WorldData = new(() =>
|
||||
{
|
||||
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(Dalamud.Game.ClientLanguage.English)!
|
||||
@@ -131,24 +119,17 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
|
||||
{
|
||||
if (clientState.IsPvP) return;
|
||||
if (!ResolvePairAddress(msg.Pair, out var pair, out var addr)) return;
|
||||
var name = msg.Pair.PlayerName;
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
var addr = _playerCharas.FirstOrDefault(f => string.Equals(f.Value.Name, name, StringComparison.Ordinal)).Value.Address;
|
||||
if (addr == nint.Zero) return;
|
||||
var useFocusTarget = _configService.Current.UseFocusTarget;
|
||||
_ = RunOnFrameworkThread(() =>
|
||||
{
|
||||
var gameObject = CreateGameObject(addr);
|
||||
if (gameObject is null) return;
|
||||
if (useFocusTarget)
|
||||
{
|
||||
_targetManager.FocusTarget = gameObject;
|
||||
if (_FocusPairIdent.HasValue && _FocusPairIdent.Value.Equals(pair.UniqueIdent))
|
||||
{
|
||||
_FocusOriginalTarget = _targetManager.FocusTarget;
|
||||
}
|
||||
}
|
||||
targetManager.FocusTarget = CreateGameObject(addr);
|
||||
else
|
||||
{
|
||||
_targetManager.Target = gameObject;
|
||||
}
|
||||
targetManager.Target = CreateGameObject(addr);
|
||||
}).ConfigureAwait(false);
|
||||
});
|
||||
IsWine = Util.IsWine();
|
||||
@@ -158,61 +139,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
private Lazy<ulong> RebuildCID() => new(GetCID);
|
||||
|
||||
public bool IsWine { get; init; }
|
||||
private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address)
|
||||
{
|
||||
resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair;
|
||||
address = nint.Zero;
|
||||
var name = resolvedPair.PlayerName;
|
||||
if (string.IsNullOrEmpty(name)) return false;
|
||||
if (!_actorObjectService.TryGetPlayerByName(name, out var descriptor))
|
||||
return false;
|
||||
address = descriptor.Address;
|
||||
return address != nint.Zero;
|
||||
}
|
||||
|
||||
public void FocusVisiblePair(Pair pair)
|
||||
{
|
||||
if (_clientState.IsPvP) return;
|
||||
if (!ResolvePairAddress(pair, out var resolvedPair, out var address)) return;
|
||||
_ = RunOnFrameworkThread(() => FocusPairUnsafe(address, resolvedPair.UniqueIdent));
|
||||
}
|
||||
|
||||
public void ReleaseVisiblePairFocus()
|
||||
{
|
||||
_ = RunOnFrameworkThread(ReleaseFocusUnsafe);
|
||||
}
|
||||
|
||||
private void FocusPairUnsafe(nint address, PairUniqueIdentifier pairIdent)
|
||||
{
|
||||
var target = CreateGameObject(address);
|
||||
if (target is null) return;
|
||||
|
||||
if (!_FocusPairIdent.HasValue)
|
||||
{
|
||||
_FocusOriginalTarget = _targetManager.FocusTarget;
|
||||
}
|
||||
|
||||
_targetManager.FocusTarget = target;
|
||||
_FocusPairIdent = pairIdent;
|
||||
}
|
||||
|
||||
private void ReleaseFocusUnsafe()
|
||||
{
|
||||
if (!_FocusPairIdent.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var previous = _FocusOriginalTarget;
|
||||
if (previous != null && !IsObjectPresent(previous))
|
||||
{
|
||||
previous = null;
|
||||
}
|
||||
|
||||
_targetManager.FocusTarget = previous;
|
||||
_FocusPairIdent = null;
|
||||
_FocusOriginalTarget = null;
|
||||
}
|
||||
|
||||
public unsafe GameObject* GposeTarget
|
||||
{
|
||||
@@ -268,7 +194,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
var objTableObj = _objectTable[index];
|
||||
if (objTableObj!.ObjectKind != DalamudObjectKind.Player) return null;
|
||||
if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null;
|
||||
return (ICharacter)objTableObj;
|
||||
}
|
||||
|
||||
@@ -300,19 +226,13 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
|
||||
{
|
||||
foreach (var actor in _actorObjectService.PlayerDescriptors
|
||||
.Where(a => a.ObjectKind == DalamudObjectKind.Player && a.ObjectIndex > 200))
|
||||
{
|
||||
var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter;
|
||||
if (character != null)
|
||||
yield return character;
|
||||
}
|
||||
return _objectTable.Where(o => o.ObjectIndex > 200 && o.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player).Cast<ICharacter>();
|
||||
}
|
||||
|
||||
public bool GetIsPlayerPresent()
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid();
|
||||
return _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid();
|
||||
}
|
||||
|
||||
public async Task<bool> GetIsPlayerPresentAsync()
|
||||
@@ -325,28 +245,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
EnsureIsOnFramework();
|
||||
playerPointer ??= GetPlayerPtr();
|
||||
if (playerPointer == IntPtr.Zero) return IntPtr.Zero;
|
||||
|
||||
var playerAddress = playerPointer.Value;
|
||||
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
||||
if (ownerEntityId == 0) return IntPtr.Zero;
|
||||
|
||||
if (playerAddress == _actorObjectService.LocalPlayerAddress)
|
||||
{
|
||||
var localOwned = _actorObjectService.LocalMinionOrMountAddress;
|
||||
if (localOwned != nint.Zero)
|
||||
{
|
||||
return localOwned;
|
||||
}
|
||||
}
|
||||
|
||||
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
|
||||
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
||||
if (ownedObject != nint.Zero)
|
||||
{
|
||||
return ownedObject;
|
||||
}
|
||||
|
||||
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||
return _objectTable.GetObjectAddress(((GameObject*)playerPointer)->ObjectIndex + 1);
|
||||
}
|
||||
|
||||
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
||||
@@ -369,62 +268,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return await RunOnFrameworkThread(() => GetPetPtr(playerPointer)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private unsafe nint FindOwnedObject(uint ownerEntityId, nint ownerAddress, Func<DalamudObjectKind, bool> matchesKind)
|
||||
{
|
||||
if (ownerEntityId == 0)
|
||||
{
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj is null || obj.Address == nint.Zero || obj.Address == ownerAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!matchesKind(obj.ObjectKind))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidate = (GameObject*)obj.Address;
|
||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
||||
{
|
||||
return obj.Address;
|
||||
}
|
||||
}
|
||||
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
private static unsafe uint ResolveOwnerId(GameObject* gameObject)
|
||||
{
|
||||
if (gameObject == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (gameObject->OwnerId != 0)
|
||||
{
|
||||
return gameObject->OwnerId;
|
||||
}
|
||||
|
||||
var character = (Character*)gameObject;
|
||||
if (character == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (character->CompanionOwnerId != 0)
|
||||
{
|
||||
return character->CompanionOwnerId;
|
||||
}
|
||||
|
||||
var parent = character->GetParentCharacter();
|
||||
return parent != null ? parent->EntityId : 0;
|
||||
}
|
||||
|
||||
public async Task<IPlayerCharacter> GetPlayerCharacterAsync()
|
||||
{
|
||||
return await RunOnFrameworkThread(GetPlayerCharacter).ConfigureAwait(false);
|
||||
@@ -433,20 +276,19 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
public IPlayerCharacter GetPlayerCharacter()
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
return _objectTable.LocalPlayer!;
|
||||
return _clientState.LocalPlayer!;
|
||||
}
|
||||
|
||||
public IntPtr GetPlayerCharacterFromCachedTableByIdent(string characterName)
|
||||
{
|
||||
if (_actorObjectService.TryGetValidatedActorByHash(characterName, out var actor))
|
||||
return actor.Address;
|
||||
if (_playerCharas.TryGetValue(characterName, out var pchar)) return pchar.Address;
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
public string GetPlayerName()
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
return _objectTable.LocalPlayer?.Name.ToString() ?? "--";
|
||||
return _clientState.LocalPlayer?.Name.ToString() ?? "--";
|
||||
}
|
||||
|
||||
public async Task<string> GetPlayerNameAsync()
|
||||
@@ -471,24 +313,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return await RunOnFrameworkThread(() => _cid.Value.ToString().GetHash256()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static unsafe bool TryGetHashedCID(IPlayerCharacter? playerCharacter, out string hashedCid)
|
||||
{
|
||||
hashedCid = string.Empty;
|
||||
if (playerCharacter == null)
|
||||
return false;
|
||||
|
||||
var address = playerCharacter.Address;
|
||||
if (address == nint.Zero)
|
||||
return false;
|
||||
|
||||
var cid = ((BattleChara*)address)->Character.ContentId;
|
||||
if (cid == 0)
|
||||
return false;
|
||||
|
||||
hashedCid = cid.ToString().GetHash256();
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
|
||||
{
|
||||
return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256();
|
||||
@@ -497,7 +321,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
public IntPtr GetPlayerPtr()
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
return _objectTable.LocalPlayer?.Address ?? IntPtr.Zero;
|
||||
return _clientState.LocalPlayer?.Address ?? IntPtr.Zero;
|
||||
}
|
||||
|
||||
public async Task<IntPtr> GetPlayerPointerAsync()
|
||||
@@ -508,13 +332,13 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
public uint GetHomeWorldId()
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
return _objectTable.LocalPlayer?.HomeWorld.RowId ?? 0;
|
||||
return _clientState.LocalPlayer?.HomeWorld.RowId ?? 0;
|
||||
}
|
||||
|
||||
public uint GetWorldId()
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
return _objectTable.LocalPlayer!.CurrentWorld.RowId;
|
||||
return _clientState.LocalPlayer!.CurrentWorld.RowId;
|
||||
}
|
||||
|
||||
public unsafe LocationInfo GetMapData()
|
||||
@@ -523,8 +347,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
var agentMap = AgentMap.Instance();
|
||||
var houseMan = HousingManager.Instance();
|
||||
uint serverId = 0;
|
||||
if (_objectTable.LocalPlayer == null) serverId = 0;
|
||||
else serverId = _objectTable.LocalPlayer.CurrentWorld.RowId;
|
||||
if (_clientState.LocalPlayer == null) serverId = 0;
|
||||
else serverId = _clientState.LocalPlayer.CurrentWorld.RowId;
|
||||
uint mapId = agentMap == null ? 0 : agentMap->CurrentMapId;
|
||||
uint territoryId = agentMap == null ? 0 : agentMap->CurrentTerritoryId;
|
||||
uint divisionId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentDivision());
|
||||
@@ -612,13 +436,17 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
|
||||
await _performanceCollector.LogPerformance(this, $"RunOnFramework:Act/{fileName}>{callerMember}:{callerLineNumber}", async () =>
|
||||
{
|
||||
if (_framework.IsInFrameworkUpdateThread)
|
||||
if (!_framework.IsInFrameworkUpdateThread)
|
||||
{
|
||||
act();
|
||||
return;
|
||||
await _framework.RunOnFrameworkThread(act).ContinueWith((_) => Task.CompletedTask).ConfigureAwait(false);
|
||||
while (_framework.IsInFrameworkUpdateThread) // yield the thread again, should technically never be triggered
|
||||
{
|
||||
_logger.LogTrace("Still on framework");
|
||||
await Task.Delay(1).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
await _framework.RunOnFrameworkThread(act).ConfigureAwait(false);
|
||||
else
|
||||
act();
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -627,12 +455,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
var fileName = Path.GetFileNameWithoutExtension(callerFilePath);
|
||||
return await _performanceCollector.LogPerformance(this, $"RunOnFramework:Func<{typeof(T)}>/{fileName}>{callerMember}:{callerLineNumber}", async () =>
|
||||
{
|
||||
if (_framework.IsInFrameworkUpdateThread)
|
||||
if (!_framework.IsInFrameworkUpdateThread)
|
||||
{
|
||||
return func.Invoke();
|
||||
var result = await _framework.RunOnFrameworkThread(func).ContinueWith((task) => task.Result).ConfigureAwait(false);
|
||||
while (_framework.IsInFrameworkUpdateThread) // yield the thread again, should technically never be triggered
|
||||
{
|
||||
_logger.LogTrace("Still on framework");
|
||||
await Task.Delay(1).ConfigureAwait(false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return await _framework.RunOnFrameworkThread(func).ConfigureAwait(false);
|
||||
return func.Invoke();
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -642,7 +476,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_framework.Update += FrameworkOnUpdate;
|
||||
if (IsLoggedIn)
|
||||
{
|
||||
_classJobId = _objectTable.LocalPlayer!.ClassJob.RowId;
|
||||
_classJobId = _clientState.LocalPlayer!.ClassJob.RowId;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Started DalamudUtilService");
|
||||
@@ -655,17 +489,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
Mediator.UnsubscribeAll(this);
|
||||
_framework.Update -= FrameworkOnUpdate;
|
||||
if (_FocusPairIdent.HasValue)
|
||||
{
|
||||
if (_framework.IsInFrameworkUpdateThread)
|
||||
{
|
||||
ReleaseFocusUnsafe();
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = RunOnFrameworkThread(ReleaseFocusUnsafe);
|
||||
}
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -690,11 +513,15 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
|
||||
curWaitTime += tick;
|
||||
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
|
||||
await Task.Delay(tick).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime);
|
||||
}
|
||||
catch (NullReferenceException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
|
||||
}
|
||||
catch (AccessViolationException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
|
||||
@@ -708,7 +535,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
const int tick = 250;
|
||||
int curWaitTime = 0;
|
||||
_logger.LogTrace("RenderFlags: {flags}", obj->RenderFlags.ToString("X"));
|
||||
while (obj->RenderFlags != VisibilityFlags.None && curWaitTime < timeOut)
|
||||
while (obj->RenderFlags != 0x00 && curWaitTime < timeOut)
|
||||
{
|
||||
_logger.LogTrace($"Waiting for gpose actor to finish drawing");
|
||||
curWaitTime += tick;
|
||||
@@ -725,12 +552,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
internal (string Name, nint Address) FindPlayerByNameHash(string ident)
|
||||
{
|
||||
if (_actorObjectService.TryGetValidatedActorByHash(ident, out var descriptor))
|
||||
{
|
||||
return (descriptor.Name, descriptor.Address);
|
||||
}
|
||||
|
||||
return default;
|
||||
_playerCharas.TryGetValue(ident, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public string? GetWorldNameFromPlayerAddress(nint address)
|
||||
@@ -753,7 +576,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
bool isDrawingChanged = false;
|
||||
if ((nint)drawObj != IntPtr.Zero)
|
||||
{
|
||||
isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None;
|
||||
isDrawing = gameObj->RenderFlags == 0b100000000000;
|
||||
if (!isDrawing)
|
||||
{
|
||||
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
||||
@@ -806,7 +629,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
private unsafe void FrameworkOnUpdateInternal()
|
||||
{
|
||||
if ((_objectTable.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
|
||||
if ((_clientState.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -816,43 +639,37 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_performanceCollector.LogPerformance(this, $"FrameworkOnUpdateInternal+{(isNormalFrameworkUpdate ? "Regular" : "Delayed")}", () =>
|
||||
{
|
||||
IsAnythingDrawing = false;
|
||||
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
||||
_performanceCollector.LogPerformance(this, $"ObjTableToCharas",
|
||||
() =>
|
||||
{
|
||||
_actorObjectService.RefreshTrackedActors();
|
||||
_notUpdatedCharas.AddRange(_playerCharas.Keys);
|
||||
|
||||
var playerDescriptors = _actorObjectService.PlayerCharacterDescriptors;
|
||||
for (var i = 0; i < playerDescriptors.Count; i++)
|
||||
for (int i = 0; i < 200; i += 2)
|
||||
{
|
||||
var actor = playerDescriptors[i];
|
||||
|
||||
var playerAddress = actor.Address;
|
||||
if (playerAddress == nint.Zero)
|
||||
var chara = _objectTable[i];
|
||||
if (chara == null || chara.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player)
|
||||
continue;
|
||||
|
||||
if (actor.ObjectIndex >= 200)
|
||||
continue;
|
||||
|
||||
if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, out bool firstTime) && firstTime)
|
||||
if (_blockedCharacterHandler.IsCharacterBlocked(chara.Address, out bool firstTime) && firstTime)
|
||||
{
|
||||
_logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X"));
|
||||
_logger.LogTrace("Skipping character {addr}, blocked/muted", chara.Address.ToString("X"));
|
||||
continue;
|
||||
}
|
||||
|
||||
var charaName = ((GameObject*)chara.Address)->NameString;
|
||||
var hash = GetHashedCIDFromPlayerPointer(chara.Address);
|
||||
if (!IsAnythingDrawing)
|
||||
{
|
||||
var gameObj = (GameObject*)playerAddress;
|
||||
var currentName = gameObj != null ? gameObj->NameString ?? string.Empty : string.Empty;
|
||||
var charaName = string.IsNullOrEmpty(currentName) ? actor.Name : currentName;
|
||||
CheckCharacterForDrawing(playerAddress, charaName);
|
||||
if (IsAnythingDrawing)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
CheckCharacterForDrawing(chara.Address, charaName);
|
||||
_notUpdatedCharas.Remove(hash);
|
||||
_playerCharas[hash] = (charaName, chara.Address);
|
||||
}
|
||||
|
||||
foreach (var notUpdatedChara in _notUpdatedCharas)
|
||||
{
|
||||
_playerCharas.Remove(notUpdatedChara);
|
||||
}
|
||||
|
||||
_notUpdatedCharas.Clear();
|
||||
});
|
||||
|
||||
if (!IsAnythingDrawing && !string.IsNullOrEmpty(_lastGlobalBlockPlayer))
|
||||
@@ -862,75 +679,76 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_lastGlobalBlockReason = string.Empty;
|
||||
}
|
||||
|
||||
// Checks on conditions
|
||||
var shouldBeInGpose = _clientState.IsGPosing;
|
||||
var shouldBeInCombat = _condition[ConditionFlag.InCombat] && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat;
|
||||
var shouldBePerforming = _condition[ConditionFlag.Performing] && _playerPerformanceConfigService.Current.PauseWhilePerforming;
|
||||
var shouldBeInInstance = _condition[ConditionFlag.BoundByDuty] && _playerPerformanceConfigService.Current.PauseInInstanceDuty;
|
||||
var shouldBeInCutscene = _condition[ConditionFlag.WatchingCutscene];
|
||||
if (_clientState.IsGPosing && !IsInGpose)
|
||||
{
|
||||
_logger.LogDebug("Gpose start");
|
||||
IsInGpose = true;
|
||||
Mediator.Publish(new GposeStartMessage());
|
||||
}
|
||||
else if (!_clientState.IsGPosing && IsInGpose)
|
||||
{
|
||||
_logger.LogDebug("Gpose end");
|
||||
IsInGpose = false;
|
||||
Mediator.Publish(new GposeEndMessage());
|
||||
}
|
||||
|
||||
// Gpose
|
||||
HandleStateTransition(() => IsInGpose, v => IsInGpose = v, shouldBeInGpose, "Gpose",
|
||||
onEnter: () =>
|
||||
{
|
||||
Mediator.Publish(new GposeStartMessage());
|
||||
},
|
||||
onExit: () =>
|
||||
{
|
||||
Mediator.Publish(new GposeEndMessage());
|
||||
});
|
||||
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.InCombat]) && IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat)
|
||||
{
|
||||
_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)));
|
||||
}
|
||||
|
||||
// Combat
|
||||
HandleStateTransition(() => IsInCombat, v => IsInCombat = v, shouldBeInCombat, "Combat",
|
||||
onEnter: () =>
|
||||
{
|
||||
Mediator.Publish(new CombatStartMessage());
|
||||
Mediator.Publish(new HaltScanMessage(nameof(IsInCombat)));
|
||||
},
|
||||
onExit: () =>
|
||||
{
|
||||
Mediator.Publish(new CombatEndMessage());
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(IsInCombat)));
|
||||
});
|
||||
|
||||
// Performance
|
||||
HandleStateTransition(() => IsPerforming, v => IsPerforming = v, shouldBePerforming, "Performance",
|
||||
onEnter: () =>
|
||||
{
|
||||
Mediator.Publish(new PerformanceStartMessage());
|
||||
Mediator.Publish(new HaltScanMessage(nameof(IsPerforming)));
|
||||
},
|
||||
onExit: () =>
|
||||
{
|
||||
Mediator.Publish(new PerformanceEndMessage());
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(IsPerforming)));
|
||||
});
|
||||
|
||||
// Instance / Duty
|
||||
HandleStateTransition(() => IsInInstance, v => IsInInstance = v, shouldBeInInstance, "Instance",
|
||||
onEnter: () =>
|
||||
{
|
||||
Mediator.Publish(new InstanceOrDutyStartMessage());
|
||||
Mediator.Publish(new HaltScanMessage(nameof(IsInInstance)));
|
||||
},
|
||||
onExit: () =>
|
||||
{
|
||||
Mediator.Publish(new InstanceOrDutyEndMessage());
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(IsInInstance)));
|
||||
});
|
||||
|
||||
// Cutscene
|
||||
HandleStateTransition(() => IsInCutscene,v => IsInCutscene = v, shouldBeInCutscene, "Cutscene",
|
||||
onEnter: () =>
|
||||
{
|
||||
Mediator.Publish(new CutsceneStartMessage());
|
||||
Mediator.Publish(new HaltScanMessage(nameof(IsInCutscene)));
|
||||
},
|
||||
onExit: () =>
|
||||
{
|
||||
Mediator.Publish(new CutsceneEndMessage());
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(IsInCutscene)));
|
||||
});
|
||||
if (_condition[ConditionFlag.WatchingCutscene] && !IsInCutscene)
|
||||
{
|
||||
_logger.LogDebug("Cutscene start");
|
||||
IsInCutscene = true;
|
||||
Mediator.Publish(new CutsceneStartMessage());
|
||||
Mediator.Publish(new HaltScanMessage(nameof(IsInCutscene)));
|
||||
}
|
||||
else if (!_condition[ConditionFlag.WatchingCutscene] && IsInCutscene)
|
||||
{
|
||||
_logger.LogDebug("Cutscene end");
|
||||
IsInCutscene = false;
|
||||
Mediator.Publish(new CutsceneEndMessage());
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(IsInCutscene)));
|
||||
}
|
||||
|
||||
if (IsInCutscene)
|
||||
{
|
||||
@@ -964,22 +782,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas)));
|
||||
}
|
||||
|
||||
var localPlayer = _objectTable.LocalPlayer;
|
||||
var localPlayer = _clientState.LocalPlayer;
|
||||
if (localPlayer != null)
|
||||
{
|
||||
_classJobId = localPlayer.ClassJob.RowId;
|
||||
|
||||
var currentWorldId = (ushort)localPlayer.CurrentWorld.RowId;
|
||||
if (currentWorldId != _lastWorldId)
|
||||
{
|
||||
var previousWorldId = _lastWorldId;
|
||||
_lastWorldId = currentWorldId;
|
||||
Mediator.Publish(new WorldChangedMessage(previousWorldId, currentWorldId));
|
||||
}
|
||||
}
|
||||
else if (_lastWorldId != 0)
|
||||
{
|
||||
_lastWorldId = 0;
|
||||
}
|
||||
|
||||
if (!IsInCombat || !IsPerforming || !IsInInstance)
|
||||
@@ -995,7 +801,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_logger.LogDebug("Logged in");
|
||||
IsLoggedIn = true;
|
||||
_lastZone = _clientState.TerritoryType;
|
||||
_lastWorldId = (ushort)localPlayer.CurrentWorld.RowId;
|
||||
_cid = RebuildCID();
|
||||
Mediator.Publish(new DalamudLoginMessage());
|
||||
}
|
||||
@@ -1003,7 +808,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
_logger.LogDebug("Logged out");
|
||||
IsLoggedIn = false;
|
||||
_lastWorldId = 0;
|
||||
Mediator.Publish(new DalamudLogoutMessage());
|
||||
}
|
||||
|
||||
@@ -1021,31 +825,4 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for the transition of different states of game
|
||||
/// </summary>
|
||||
/// <param name="getState">Get state of condition</param>
|
||||
/// <param name="setState">Set state of condition</param>
|
||||
/// <param name="shouldBeActive">Correction of the state of the condition</param>
|
||||
/// <param name="stateName">Condition name</param>
|
||||
/// <param name="onEnter">Function for on entering the state</param>
|
||||
/// <param name="onExit">Function for on leaving the state</param>
|
||||
private void HandleStateTransition(Func<bool> getState, Action<bool> setState, bool shouldBeActive, string stateName, System.Action onEnter, System.Action onExit)
|
||||
{
|
||||
var isActive = getState();
|
||||
|
||||
if (shouldBeActive && !isActive)
|
||||
{
|
||||
_logger.LogDebug("{stateName} start", stateName);
|
||||
setState(true);
|
||||
onEnter();
|
||||
}
|
||||
else if (!shouldBeActive && isActive)
|
||||
{
|
||||
_logger.LogDebug("{stateName} end", stateName);
|
||||
setState(false);
|
||||
onExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ public record Event
|
||||
{
|
||||
public DateTime EventTime { get; }
|
||||
public string UID { get; }
|
||||
public string AliasOrUid { get; }
|
||||
public string UserId { get; }
|
||||
public string Character { get; }
|
||||
public string EventSource { get; }
|
||||
public EventSeverity EventSeverity { get; }
|
||||
@@ -16,9 +14,7 @@ public record Event
|
||||
public Event(string? Character, UserData UserData, string EventSource, EventSeverity EventSeverity, string Message)
|
||||
{
|
||||
EventTime = DateTime.Now;
|
||||
this.UserId = UserData.UID;
|
||||
this.AliasOrUid = UserData.AliasOrUID;
|
||||
this.UID = UserData.UID;
|
||||
this.UID = UserData.AliasOrUID;
|
||||
this.Character = Character ?? string.Empty;
|
||||
this.EventSource = EventSource;
|
||||
this.EventSeverity = EventSeverity;
|
||||
@@ -41,7 +37,7 @@ public record Event
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(Character))
|
||||
return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{AliasOrUid}> {Message}";
|
||||
return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{UID}> {Message}";
|
||||
else
|
||||
return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{UID}\\{Character}> {Message}";
|
||||
}
|
||||
|
||||
@@ -1,692 +0,0 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.Rendering;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.UtilsEnum.Enum;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Pictomancy;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace LightlessSync.Services.LightFinder;
|
||||
|
||||
public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
private readonly ILogger<LightFinderPlateHandler> _logger;
|
||||
private readonly IAddonLifecycle _addonLifecycle;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly LightlessMediator _mediator;
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
private readonly IUiBuilder _uiBuilder;
|
||||
private bool _mEnabled;
|
||||
private bool _needsLabelRefresh;
|
||||
private bool _drawSubscribed;
|
||||
private AddonNamePlate* _mpNameplateAddon;
|
||||
private readonly object _labelLock = new();
|
||||
private readonly NameplateBuffers _buffers = new();
|
||||
private int _labelRenderCount;
|
||||
|
||||
private const string DefaultLabelText = "LightFinder";
|
||||
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
||||
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
||||
private static readonly Vector2 DefaultPivot = new(0.5f, 1f);
|
||||
|
||||
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
||||
|
||||
public LightFinderPlateHandler(
|
||||
ILogger<LightFinderPlateHandler> logger,
|
||||
IAddonLifecycle addonLifecycle,
|
||||
IGameGui gameGui,
|
||||
LightlessConfigService configService,
|
||||
LightlessMediator mediator,
|
||||
IObjectTable objectTable,
|
||||
PairUiService pairUiService,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
PictomancyService pictomancyService)
|
||||
{
|
||||
_logger = logger;
|
||||
_addonLifecycle = addonLifecycle;
|
||||
_gameGui = gameGui;
|
||||
_configService = configService;
|
||||
_mediator = mediator;
|
||||
_objectTable = objectTable;
|
||||
_pairUiService = pairUiService;
|
||||
_uiBuilder = pluginInterface.UiBuilder ?? throw new ArgumentNullException(nameof(pluginInterface));
|
||||
_ = pictomancyService ?? throw new ArgumentNullException(nameof(pictomancyService));
|
||||
|
||||
}
|
||||
|
||||
internal void Init()
|
||||
{
|
||||
if (!_drawSubscribed)
|
||||
{
|
||||
_uiBuilder.Draw += OnUiBuilderDraw;
|
||||
_drawSubscribed = true;
|
||||
}
|
||||
|
||||
EnableNameplate();
|
||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
||||
}
|
||||
|
||||
internal void Uninit()
|
||||
{
|
||||
DisableNameplate();
|
||||
if (_drawSubscribed)
|
||||
{
|
||||
_uiBuilder.Draw -= OnUiBuilderDraw;
|
||||
_drawSubscribed = false;
|
||||
}
|
||||
ClearLabelBuffer();
|
||||
_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(e, "Unknown error while trying to enable nameplate.");
|
||||
DisableNameplate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void DisableNameplate()
|
||||
{
|
||||
if (_mEnabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
_addonLifecycle.UnregisterListener(NameplateDrawDetour);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Unknown error while unregistering nameplate listener.");
|
||||
}
|
||||
|
||||
_mEnabled = false;
|
||||
ClearNameplateCaches();
|
||||
}
|
||||
}
|
||||
|
||||
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
|
||||
{
|
||||
if (args.Addon.Address == nint.Zero)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning))
|
||||
_logger.LogWarning("Nameplate draw detour received a null addon address, skipping update.");
|
||||
return;
|
||||
}
|
||||
|
||||
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
||||
|
||||
if (_mpNameplateAddon != pNameplateAddon)
|
||||
{
|
||||
ClearNameplateCaches();
|
||||
_mpNameplateAddon = pNameplateAddon;
|
||||
}
|
||||
|
||||
UpdateNameplateNodes();
|
||||
}
|
||||
|
||||
private void UpdateNameplateNodes()
|
||||
{
|
||||
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
||||
if (currentHandle.Address == nint.Zero)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh.");
|
||||
ClearLabelBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
var currentAddon = (AddonNamePlate*)currentHandle.Address;
|
||||
if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon)
|
||||
{
|
||||
if (_mpNameplateAddon != null && _logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon);
|
||||
return;
|
||||
}
|
||||
|
||||
var framework = Framework.Instance();
|
||||
if (framework == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Framework instance unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var uiModule = framework->GetUIModule();
|
||||
if (uiModule == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("UI module unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var ui3DModule = uiModule->GetUI3DModule();
|
||||
if (ui3DModule == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("UI3D module unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var vec = ui3DModule->NamePlateObjectInfoPointers;
|
||||
if (vec.IsEmpty)
|
||||
{
|
||||
ClearLabelBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
var visibleUserIdsSnapshot = VisibleUserIds;
|
||||
var safeCount = System.Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
|
||||
var currentConfig = _configService.Current;
|
||||
var labelColor = UIColors.Get("Lightfinder");
|
||||
var edgeColor = UIColors.Get("LightfinderEdge");
|
||||
var scratchCount = 0;
|
||||
|
||||
for (int i = 0; i < safeCount; ++i)
|
||||
{
|
||||
var objectInfoPtr = vec[i];
|
||||
if (objectInfoPtr == null)
|
||||
continue;
|
||||
|
||||
var objectInfo = objectInfoPtr.Value;
|
||||
if (objectInfo == null || objectInfo->GameObject == null)
|
||||
continue;
|
||||
|
||||
var nameplateIndex = objectInfo->NamePlateIndex;
|
||||
if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
|
||||
continue;
|
||||
|
||||
var gameObject = objectInfo->GameObject;
|
||||
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
|
||||
continue;
|
||||
|
||||
// CID gating
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
|
||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||
continue;
|
||||
|
||||
var local = _objectTable.LocalPlayer;
|
||||
if (!currentConfig.LightfinderLabelShowOwn && local != null &&
|
||||
objectInfo->GameObject->GetGameObjectId() == local.GameObjectId)
|
||||
continue;
|
||||
|
||||
var hidePaired = !currentConfig.LightfinderLabelShowPaired;
|
||||
var goId = gameObject->GetGameObjectId();
|
||||
if (hidePaired && visibleUserIdsSnapshot.Contains(goId))
|
||||
continue;
|
||||
|
||||
var nameplateObject = _mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
|
||||
var root = nameplateObject.RootComponentNode;
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
var nameText = nameplateObject.NameText;
|
||||
var marker = nameplateObject.MarkerIcon;
|
||||
|
||||
if (root == null || root->Component == null || nameContainer == null || nameText == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
root->Component->UldManager.UpdateDrawNodeList();
|
||||
|
||||
bool isVisible =
|
||||
(marker != null && marker->AtkResNode.IsVisible()) ||
|
||||
(nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) ||
|
||||
currentConfig.LightfinderLabelShowHidden;
|
||||
|
||||
if (!isVisible)
|
||||
continue;
|
||||
|
||||
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
|
||||
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||
var effectiveScale = baseScale * scaleMultiplier;
|
||||
var baseFontSize = currentConfig.LightfinderLabelUseIcon ? 36f : 24f;
|
||||
var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
||||
var labelContent = currentConfig.LightfinderLabelUseIcon
|
||||
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
|
||||
: DefaultLabelText;
|
||||
|
||||
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
||||
labelContent = DefaultLabelText;
|
||||
|
||||
var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||
AlignmentType alignment;
|
||||
|
||||
var textScaleY = nameText->AtkResNode.ScaleY;
|
||||
if (textScaleY <= 0f)
|
||||
textScaleY = 1f;
|
||||
|
||||
var blockHeight = ResolveCache(
|
||||
_buffers.TextHeights,
|
||||
nameplateIndex,
|
||||
System.Math.Abs((int)nameplateObject.TextH),
|
||||
() => GetScaledTextHeight(nameText),
|
||||
nodeHeight);
|
||||
|
||||
var containerHeight = ResolveCache(
|
||||
_buffers.ContainerHeights,
|
||||
nameplateIndex,
|
||||
(int)nameContainer->Height,
|
||||
() =>
|
||||
{
|
||||
var computed = blockHeight + (int)System.Math.Round(8 * textScaleY);
|
||||
return computed <= blockHeight ? blockHeight + 1 : computed;
|
||||
},
|
||||
blockHeight + 1);
|
||||
|
||||
var blockTop = containerHeight - blockHeight;
|
||||
if (blockTop < 0)
|
||||
blockTop = 0;
|
||||
var verticalPadding = (int)System.Math.Round(4 * effectiveScale);
|
||||
|
||||
var positionY = blockTop - verticalPadding;
|
||||
|
||||
var rawTextWidth = (int)nameplateObject.TextW;
|
||||
var textWidth = ResolveCache(
|
||||
_buffers.TextWidths,
|
||||
nameplateIndex,
|
||||
System.Math.Abs(rawTextWidth),
|
||||
() => GetScaledTextWidth(nameText),
|
||||
nodeWidth);
|
||||
|
||||
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
||||
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
|
||||
|
||||
if (nameContainer == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Nameplate {Index} container became unavailable during update, skipping.", nameplateIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
float finalX;
|
||||
if (currentConfig.LightfinderAutoAlign)
|
||||
{
|
||||
var measuredWidth = System.Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
|
||||
var measuredWidthF = (float)measuredWidth;
|
||||
var alignmentType = currentConfig.LabelAlignment;
|
||||
|
||||
var containerScale = nameContainer->ScaleX;
|
||||
if (containerScale <= 0f)
|
||||
containerScale = 1f;
|
||||
var containerWidthRaw = (float)nameContainer->Width;
|
||||
if (containerWidthRaw <= 0f)
|
||||
containerWidthRaw = measuredWidthF;
|
||||
var containerWidth = containerWidthRaw * containerScale;
|
||||
if (containerWidth <= 0f)
|
||||
containerWidth = measuredWidthF;
|
||||
|
||||
var containerLeft = nameContainer->ScreenX;
|
||||
var containerRight = containerLeft + containerWidth;
|
||||
var containerCenter = containerLeft + (containerWidth * 0.5f);
|
||||
|
||||
var iconMargin = currentConfig.LightfinderLabelUseIcon
|
||||
? System.Math.Min(containerWidth * 0.1f, 14f * containerScale)
|
||||
: 0f;
|
||||
|
||||
switch (alignmentType)
|
||||
{
|
||||
case LabelAlignment.Left:
|
||||
finalX = containerLeft + iconMargin;
|
||||
alignment = AlignmentType.BottomLeft;
|
||||
break;
|
||||
case LabelAlignment.Right:
|
||||
finalX = containerRight - iconMargin;
|
||||
alignment = AlignmentType.BottomRight;
|
||||
break;
|
||||
default:
|
||||
finalX = containerCenter;
|
||||
alignment = AlignmentType.Bottom;
|
||||
break;
|
||||
}
|
||||
|
||||
finalX += currentConfig.LightfinderLabelOffsetX;
|
||||
}
|
||||
else
|
||||
{
|
||||
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
|
||||
var hasCachedOffset = cachedTextOffset != int.MinValue;
|
||||
var baseOffsetX = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) ? cachedTextOffset : 0;
|
||||
finalX = nameContainer->ScreenX + baseOffsetX + 58 + currentConfig.LightfinderLabelOffsetX;
|
||||
alignment = AlignmentType.Bottom;
|
||||
}
|
||||
|
||||
positionY += currentConfig.LightfinderLabelOffsetY;
|
||||
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
||||
|
||||
var finalPosition = new Vector2(finalX, nameContainer->ScreenY + positionY);
|
||||
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
|
||||
? AlignmentToPivot(alignment)
|
||||
: DefaultPivot;
|
||||
var textColorPacked = PackColor(labelColor);
|
||||
var edgeColorPacked = PackColor(edgeColor);
|
||||
|
||||
_buffers.LabelScratch[scratchCount++] = new NameplateLabelInfo(
|
||||
finalPosition,
|
||||
labelContent,
|
||||
textColorPacked,
|
||||
edgeColorPacked,
|
||||
targetFontSize,
|
||||
pivot,
|
||||
currentConfig.LightfinderLabelUseIcon);
|
||||
}
|
||||
|
||||
lock (_labelLock)
|
||||
{
|
||||
if (scratchCount == 0)
|
||||
{
|
||||
_labelRenderCount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Copy(_buffers.LabelScratch, _buffers.LabelRender, scratchCount);
|
||||
_labelRenderCount = scratchCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUiBuilderDraw()
|
||||
{
|
||||
if (!_mEnabled)
|
||||
return;
|
||||
|
||||
int copyCount;
|
||||
lock (_labelLock)
|
||||
{
|
||||
copyCount = _labelRenderCount;
|
||||
if (copyCount == 0)
|
||||
return;
|
||||
|
||||
Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount);
|
||||
}
|
||||
|
||||
using var drawList = PictoService.Draw();
|
||||
if (drawList == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < copyCount; ++i)
|
||||
{
|
||||
ref var info = ref _buffers.LabelCopy[i];
|
||||
var font = default(ImFontPtr);
|
||||
if (info.UseIcon)
|
||||
{
|
||||
var ioFonts = ImGui.GetIO().Fonts;
|
||||
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
|
||||
}
|
||||
|
||||
drawList.AddScreenText(info.ScreenPosition, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch
|
||||
{
|
||||
AlignmentType.BottomLeft => new Vector2(0f, 1f),
|
||||
AlignmentType.BottomRight => new Vector2(1f, 1f),
|
||||
AlignmentType.TopLeft => new Vector2(0f, 0f),
|
||||
AlignmentType.TopRight => new Vector2(1f, 0f),
|
||||
AlignmentType.Top => new Vector2(0.5f, 0f),
|
||||
AlignmentType.Left => new Vector2(0f, 0.5f),
|
||||
AlignmentType.Right => new Vector2(1f, 0.5f),
|
||||
_ => DefaultPivot
|
||||
};
|
||||
|
||||
private static uint PackColor(Vector4 color)
|
||||
{
|
||||
var r = (byte)System.Math.Clamp(color.X * 255f, 0f, 255f);
|
||||
var g = (byte)System.Math.Clamp(color.Y * 255f, 0f, 255f);
|
||||
var b = (byte)System.Math.Clamp(color.Z * 255f, 0f, 255f);
|
||||
var a = (byte)System.Math.Clamp(color.W * 255f, 0f, 255f);
|
||||
return (uint)((a << 24) | (b << 16) | (g << 8) | r);
|
||||
}
|
||||
|
||||
private void ClearLabelBuffer()
|
||||
{
|
||||
lock (_labelLock)
|
||||
{
|
||||
_labelRenderCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe int GetScaledTextHeight(AtkTextNode* node)
|
||||
{
|
||||
if (node == null)
|
||||
return 0;
|
||||
|
||||
var resNode = &node->AtkResNode;
|
||||
var rawHeight = (int)resNode->GetHeight();
|
||||
if (rawHeight <= 0 && node->LineSpacing > 0)
|
||||
rawHeight = node->LineSpacing;
|
||||
if (rawHeight <= 0)
|
||||
rawHeight = AtkNodeHelpers.DefaultTextNodeHeight;
|
||||
|
||||
var scale = resNode->ScaleY;
|
||||
if (scale <= 0f)
|
||||
scale = 1f;
|
||||
|
||||
var computed = (int)System.Math.Round(rawHeight * scale);
|
||||
return System.Math.Max(1, computed);
|
||||
}
|
||||
|
||||
private static unsafe int GetScaledTextWidth(AtkTextNode* node)
|
||||
{
|
||||
if (node == null)
|
||||
return 0;
|
||||
|
||||
var resNode = &node->AtkResNode;
|
||||
var rawWidth = (int)resNode->GetWidth();
|
||||
if (rawWidth <= 0)
|
||||
rawWidth = AtkNodeHelpers.DefaultTextNodeWidth;
|
||||
|
||||
var scale = resNode->ScaleX;
|
||||
if (scale <= 0f)
|
||||
scale = 1f;
|
||||
|
||||
var computed = (int)System.Math.Round(rawWidth * scale);
|
||||
return System.Math.Max(1, computed);
|
||||
}
|
||||
|
||||
private static int ResolveCache(
|
||||
int[] cache,
|
||||
int index,
|
||||
int rawValue,
|
||||
Func<int> fallback,
|
||||
int fallbackWhenZero)
|
||||
{
|
||||
if (rawValue > 0)
|
||||
{
|
||||
cache[index] = rawValue;
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
var cachedValue = cache[index];
|
||||
if (cachedValue > 0)
|
||||
return cachedValue;
|
||||
|
||||
var computed = fallback();
|
||||
if (computed <= 0)
|
||||
computed = fallbackWhenZero;
|
||||
|
||||
cache[index] = computed;
|
||||
return computed;
|
||||
}
|
||||
|
||||
private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset)
|
||||
{
|
||||
if (System.Math.Abs(measuredTextWidth) > 0 || textOffset != 0)
|
||||
{
|
||||
_buffers.TextOffsets[nameplateIndex] = textOffset;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static string NormalizeIconGlyph(string? rawInput)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawInput))
|
||||
return DefaultIconGlyph;
|
||||
|
||||
var trimmed = rawInput.Trim();
|
||||
|
||||
if (Enum.TryParse<SeIconChar>(trimmed, true, out var iconEnum))
|
||||
return SeIconCharExtensions.ToIconString(iconEnum);
|
||||
|
||||
var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? trimmed[2..]
|
||||
: trimmed;
|
||||
|
||||
if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue))
|
||||
return char.ConvertFromUtf32(hexValue);
|
||||
|
||||
var enumerator = trimmed.EnumerateRunes();
|
||||
if (enumerator.MoveNext())
|
||||
return enumerator.Current.ToString();
|
||||
|
||||
return DefaultIconGlyph;
|
||||
}
|
||||
|
||||
internal static string ToIconEditorString(string? rawInput)
|
||||
{
|
||||
var normalized = NormalizeIconGlyph(rawInput);
|
||||
var runeEnumerator = normalized.EnumerateRunes();
|
||||
return runeEnumerator.MoveNext()
|
||||
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
||||
: DefaultIconGlyph;
|
||||
}
|
||||
private readonly struct NameplateLabelInfo
|
||||
{
|
||||
public NameplateLabelInfo(
|
||||
Vector2 screenPosition,
|
||||
string text,
|
||||
uint textColor,
|
||||
uint edgeColor,
|
||||
float fontSize,
|
||||
Vector2 pivot,
|
||||
bool useIcon)
|
||||
{
|
||||
ScreenPosition = screenPosition;
|
||||
Text = text;
|
||||
TextColor = textColor;
|
||||
EdgeColor = edgeColor;
|
||||
FontSize = fontSize;
|
||||
Pivot = pivot;
|
||||
UseIcon = useIcon;
|
||||
}
|
||||
|
||||
public Vector2 ScreenPosition { get; }
|
||||
public string Text { get; }
|
||||
public uint TextColor { get; }
|
||||
public uint EdgeColor { get; }
|
||||
public float FontSize { get; }
|
||||
public Vector2 Pivot { get; }
|
||||
public bool UseIcon { get; }
|
||||
}
|
||||
|
||||
private HashSet<ulong> VisibleUserIds
|
||||
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||
|
||||
public void FlagRefresh()
|
||||
{
|
||||
_needsLabelRefresh = true;
|
||||
}
|
||||
|
||||
public void OnTick(PriorityFrameworkUpdateMessage _)
|
||||
{
|
||||
if (_needsLabelRefresh)
|
||||
{
|
||||
UpdateNameplateNodes();
|
||||
_needsLabelRefresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
||||
{
|
||||
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
||||
if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet))
|
||||
return;
|
||||
|
||||
_activeBroadcastingCids = newSet;
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids));
|
||||
FlagRefresh();
|
||||
}
|
||||
|
||||
public void ClearNameplateCaches()
|
||||
{
|
||||
_buffers.Clear();
|
||||
ClearLabelBuffer();
|
||||
}
|
||||
|
||||
private sealed class NameplateBuffers
|
||||
{
|
||||
public NameplateBuffers()
|
||||
{
|
||||
TextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
System.Array.Fill(TextOffsets, int.MinValue);
|
||||
}
|
||||
|
||||
public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
public int[] TextHeights { get; } = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
public int[] ContainerHeights { get; } = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
public int[] TextOffsets { get; }
|
||||
public NameplateLabelInfo[] LabelScratch { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||
public NameplateLabelInfo[] LabelRender { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||
public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
System.Array.Clear(TextWidths, 0, TextWidths.Length);
|
||||
System.Array.Clear(TextHeights, 0, TextHeights.Length);
|
||||
System.Array.Clear(ContainerHeights, 0, ContainerHeights.Length);
|
||||
System.Array.Fill(TextOffsets, int.MinValue);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Init();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Uninit();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,575 +0,0 @@
|
||||
using Dalamud.Interface;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Models;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services.LightFinder;
|
||||
public class LightFinderService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
private readonly ILogger<LightFinderService> _logger;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly LightlessConfigService _config;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private CancellationTokenSource? _lightfinderCancelTokens;
|
||||
private Action? _connectedHandler;
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
public bool IsLightFinderAvailable { get; private set; } = false;
|
||||
|
||||
public bool IsBroadcasting => _config.Current.BroadcastEnabled;
|
||||
private bool _syncedOnStartup = false;
|
||||
private bool _waitingForTtlFetch = false;
|
||||
private TimeSpan? _remainingTtl = null;
|
||||
private DateTime _lastTtlCheck = DateTime.MinValue;
|
||||
private DateTime _lastForcedDisableTime = DateTime.MinValue;
|
||||
private static readonly TimeSpan _disableCooldown = TimeSpan.FromSeconds(5);
|
||||
public TimeSpan? RemainingTtl => _remainingTtl;
|
||||
public TimeSpan? RemainingCooldown
|
||||
{
|
||||
get
|
||||
{
|
||||
var elapsed = DateTime.UtcNow - _lastForcedDisableTime;
|
||||
if (elapsed >= _disableCooldown) return null;
|
||||
return _disableCooldown - elapsed;
|
||||
}
|
||||
}
|
||||
|
||||
public LightFinderService(ILogger<LightFinderService> 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", context);
|
||||
return;
|
||||
}
|
||||
await action().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<string?> GetLocalHashedCidAsync(string context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false);
|
||||
return cid.ToString().GetHash256();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to resolve CID for {Context}", context);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyBroadcastDisabled(bool forcePublish = false)
|
||||
{
|
||||
bool wasEnabled = _config.Current.BroadcastEnabled;
|
||||
bool hadExpiry = _config.Current.BroadcastTtl != DateTime.MinValue;
|
||||
bool hadRemaining = _remainingTtl.HasValue;
|
||||
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
|
||||
if (wasEnabled || hadExpiry)
|
||||
_config.Save();
|
||||
|
||||
_remainingTtl = null;
|
||||
_waitingForTtlFetch = false;
|
||||
_syncedOnStartup = false;
|
||||
|
||||
if (forcePublish || wasEnabled || hadRemaining)
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
|
||||
private bool TryApplyBroadcastEnabled(TimeSpan? ttl, string context)
|
||||
{
|
||||
if (ttl is not { } validTtl || validTtl <= TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogWarning("Lightfinder enable skipped ({Context}): invalid TTL ({TTL})", context, ttl);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool wasEnabled = _config.Current.BroadcastEnabled;
|
||||
TimeSpan? previousRemaining = _remainingTtl;
|
||||
DateTime previousExpiry = _config.Current.BroadcastTtl;
|
||||
|
||||
var newExpiry = DateTime.UtcNow + validTtl;
|
||||
|
||||
_config.Current.BroadcastEnabled = true;
|
||||
_config.Current.BroadcastTtl = newExpiry;
|
||||
|
||||
if (!wasEnabled || previousExpiry != newExpiry)
|
||||
_config.Save();
|
||||
|
||||
_remainingTtl = validTtl;
|
||||
_waitingForTtlFetch = false;
|
||||
|
||||
if (!wasEnabled || previousRemaining != validTtl)
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl));
|
||||
|
||||
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleLightfinderUnavailable(string message, Exception? ex = null)
|
||||
{
|
||||
if (ex != null)
|
||||
_logger.LogWarning(ex, message);
|
||||
else
|
||||
_logger.LogWarning(message);
|
||||
|
||||
IsLightFinderAvailable = false;
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
}
|
||||
|
||||
private void OnDisconnected()
|
||||
{
|
||||
IsLightFinderAvailable = false;
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogDebug("Cleared Lightfinder state due to disconnect.");
|
||||
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
|
||||
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
||||
_mediator.Subscribe<DisconnectedMessage>(this, _ => OnDisconnected());
|
||||
|
||||
IsLightFinderAvailable = false;
|
||||
|
||||
_lightfinderCancelTokens?.Cancel();
|
||||
_lightfinderCancelTokens?.Dispose();
|
||||
_lightfinderCancelTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_connectedHandler = () => _ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
|
||||
_apiController.OnConnected += _connectedHandler;
|
||||
|
||||
if (_apiController.IsConnected)
|
||||
_ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_lightfinderCancelTokens?.Cancel();
|
||||
_lightfinderCancelTokens?.Dispose();
|
||||
_lightfinderCancelTokens = null;
|
||||
|
||||
if (_connectedHandler is not null)
|
||||
{
|
||||
_apiController.OnConnected -= _connectedHandler;
|
||||
_connectedHandler = null;
|
||||
}
|
||||
|
||||
_mediator.UnsubscribeAll(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!_apiController.IsConnected && !cancellationToken.IsCancellationRequested)
|
||||
await Task.Delay(250, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var hashedCid = await GetLocalHashedCidAsync("Lightfinder state check").ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(hashedCid))
|
||||
return;
|
||||
|
||||
BroadcastStatusInfoDto? status = null;
|
||||
try
|
||||
{
|
||||
status = await _apiController.IsUserBroadcasting(hashedCid).ConfigureAwait(false);
|
||||
}
|
||||
catch (HubException ex) when (ex.Message.Contains("Method does not exist", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HandleLightfinderUnavailable("Lightfinder unavailable on server (required method missing).", ex);
|
||||
}
|
||||
|
||||
if (!IsLightFinderAvailable)
|
||||
_logger.LogInformation("Lightfinder is available.");
|
||||
|
||||
IsLightFinderAvailable = true;
|
||||
|
||||
bool isBroadcasting = status?.IsBroadcasting == true;
|
||||
TimeSpan? ttl = status?.TTL;
|
||||
|
||||
if (isBroadcasting)
|
||||
{
|
||||
if (ttl is not { } remaining || remaining <= TimeSpan.Zero)
|
||||
ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||
|
||||
if (TryApplyBroadcastEnabled(ttl, "server handshake"))
|
||||
{
|
||||
_syncedOnStartup = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isBroadcasting = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBroadcasting)
|
||||
{
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogInformation("Lightfinder is available but no active broadcast was found.");
|
||||
}
|
||||
|
||||
if (_config.Current.LightfinderAutoEnableOnConnect && !isBroadcasting)
|
||||
{
|
||||
_logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect.");
|
||||
_mediator.Publish(new EnableBroadcastMessage(hashedCid, true));
|
||||
|
||||
_mediator.Publish(new NotificationMessage(
|
||||
"Broadcast Auto-Enabled",
|
||||
"Your Lightfinder broadcast has been automatically enabled.",
|
||||
NotificationType.Info));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Lightfinder check was canceled.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleLightfinderUnavailable("Lightfinder check failed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnableBroadcast(EnableBroadcastMessage msg)
|
||||
{
|
||||
_ = RequireConnectionAsync(nameof(OnEnableBroadcast), async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
GroupBroadcastRequestDto? groupDto = null;
|
||||
if (_config.Current.SyncshellFinderEnabled && _config.Current.SelectedFinderSyncshell != null)
|
||||
{
|
||||
groupDto = new GroupBroadcastRequestDto
|
||||
{
|
||||
HashedCID = msg.HashedCid,
|
||||
GID = _config.Current.SelectedFinderSyncshell,
|
||||
Enabled = msg.Enabled,
|
||||
};
|
||||
}
|
||||
|
||||
await _apiController.SetBroadcastStatus(msg.Enabled, groupDto).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
|
||||
|
||||
if (!msg.Enabled)
|
||||
{
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
Mediator.Publish(new EventMessage(new Events.Event(nameof(LightFinderService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
return;
|
||||
}
|
||||
|
||||
_waitingForTtlFetch = true;
|
||||
|
||||
try
|
||||
{
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
|
||||
|
||||
if (TryApplyBroadcastEnabled(ttl, "client request"))
|
||||
{
|
||||
_logger.LogDebug("Fetched TTL from server: {TTL}", ttl);
|
||||
Mediator.Publish(new EventMessage(new Events.Event(nameof(LightFinderService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_waitingForTtlFetch = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to toggle broadcast for {Cid}", msg.HashedCid);
|
||||
_waitingForTtlFetch = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
||||
{
|
||||
_config.Current.BroadcastEnabled = msg.Enabled;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
public async Task<bool> CheckIfBroadcastingAsync(string targetCid)
|
||||
{
|
||||
bool result = false;
|
||||
await RequireConnectionAsync(nameof(CheckIfBroadcastingAsync), async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("[BroadcastCheck] Checking CID: {cid}", targetCid);
|
||||
|
||||
var info = await _apiController.IsUserBroadcasting(targetCid).ConfigureAwait(false);
|
||||
result = info?.TTL > TimeSpan.Zero;
|
||||
|
||||
|
||||
_logger.LogDebug("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to check broadcast status for {cid}", targetCid);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<TimeSpan?> GetBroadcastTtlAsync(string? cidForLog = null)
|
||||
{
|
||||
TimeSpan? ttl = null;
|
||||
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
|
||||
try
|
||||
{
|
||||
ttl = await _apiController.GetBroadcastTtl().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (cidForLog is { Length: > 0 })
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {Cid}", cidForLog);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL");
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
return ttl;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, BroadcastStatusInfoDto?>> AreUsersBroadcastingAsync(List<string> hashedCids)
|
||||
{
|
||||
Dictionary<string, BroadcastStatusInfoDto?> result = new(StringComparer.Ordinal);
|
||||
|
||||
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.");
|
||||
_mediator.Publish(new NotificationMessage(
|
||||
"Broadcast Unavailable",
|
||||
"Lightfinder is not available on this server.",
|
||||
NotificationType.Error));
|
||||
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);
|
||||
_mediator.Publish(new NotificationMessage(
|
||||
"Broadcast Cooldown",
|
||||
$"Please wait {cd.TotalSeconds:F0} seconds before re-enabling broadcast.",
|
||||
NotificationType.Warning));
|
||||
return;
|
||||
}
|
||||
|
||||
var hashedCid = await GetLocalHashedCidAsync(nameof(ToggleBroadcast)).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(hashedCid))
|
||||
{
|
||||
_logger.LogWarning("ToggleBroadcast - unable to resolve CID.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var isCurrentlyBroadcasting = await CheckIfBroadcastingAsync(hashedCid).ConfigureAwait(false);
|
||||
var newStatus = !isCurrentlyBroadcasting;
|
||||
|
||||
if (!newStatus)
|
||||
{
|
||||
_lastForcedDisableTime = DateTime.UtcNow;
|
||||
_logger.LogDebug("Manual disable: cooldown timer started.");
|
||||
}
|
||||
|
||||
_logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
|
||||
|
||||
_mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus));
|
||||
|
||||
_mediator.Publish(new NotificationMessage(
|
||||
newStatus ? "Broadcast Enabled" : "Broadcast Disabled",
|
||||
newStatus ? "Your Lightfinder broadcast has been enabled." : "Your Lightfinder broadcast has been disabled.",
|
||||
NotificationType.Info));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to determine current broadcast status for toggle");
|
||||
_mediator.Publish(new NotificationMessage(
|
||||
"Broadcast Toggle Failed",
|
||||
$"Failed to toggle broadcast: {ex.Message}",
|
||||
NotificationType.Error));
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnTick(PriorityFrameworkUpdateMessage _)
|
||||
{
|
||||
if (!IsLightFinderAvailable)
|
||||
return;
|
||||
|
||||
if (_config?.Current == null)
|
||||
return;
|
||||
|
||||
if ((DateTime.UtcNow - _lastTtlCheck).TotalSeconds < 1)
|
||||
return;
|
||||
|
||||
_lastTtlCheck = DateTime.UtcNow;
|
||||
|
||||
await RequireConnectionAsync(nameof(OnTick), async () => {
|
||||
if (!_syncedOnStartup && _config.Current.BroadcastEnabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
var hashedCid = await GetLocalHashedCidAsync("startup TTL refresh").ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(hashedCid))
|
||||
{
|
||||
_logger.LogDebug("Skipping TTL refresh; hashed CID unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||
if (TryApplyBroadcastEnabled(ttl, "startup TTL refresh"))
|
||||
{
|
||||
_syncedOnStartup = true;
|
||||
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", ttl);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh TTL in OnTick");
|
||||
_syncedOnStartup = false;
|
||||
}
|
||||
}
|
||||
if (_config.Current.BroadcastEnabled)
|
||||
{
|
||||
if (_waitingForTtlFetch)
|
||||
{
|
||||
_logger.LogDebug("OnTick skipped: waiting for TTL fetch");
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime expiry = _config.Current.BroadcastTtl;
|
||||
TimeSpan remaining = expiry - DateTime.UtcNow;
|
||||
_remainingTtl = remaining > TimeSpan.Zero ? remaining : null;
|
||||
if (_remainingTtl == null)
|
||||
{
|
||||
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
ShowBroadcastExpiredNotification();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_remainingTtl = null;
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void ShowBroadcastExpiredNotification()
|
||||
{
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Id = "broadcast_expired",
|
||||
Title = "Broadcast Expired",
|
||||
Message = "Your Lightfinder broadcast has expired after 3 hours. Would you like to re-enable it?",
|
||||
Type = NotificationType.PairRequest,
|
||||
Duration = TimeSpan.FromSeconds(180),
|
||||
Actions = new List<LightlessNotificationAction>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = "re_enable",
|
||||
Label = "Re-enable",
|
||||
Icon = FontAwesomeIcon.Plus,
|
||||
Color = UIColors.Get("PairBlue"),
|
||||
IsPrimary = true,
|
||||
OnClick = (n) =>
|
||||
{
|
||||
_logger.LogInformation("Re-enabling broadcast from notification");
|
||||
ToggleBroadcast();
|
||||
n.IsDismissed = true;
|
||||
n.IsAnimatingOut = true;
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "close",
|
||||
Label = "Close",
|
||||
Icon = FontAwesomeIcon.Times,
|
||||
Color = UIColors.Get("DimRed"),
|
||||
OnClick = (n) =>
|
||||
{
|
||||
_logger.LogInformation("Broadcast expiration notification dismissed");
|
||||
n.IsDismissed = true;
|
||||
n.IsAnimatingOut = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
}
|
||||
7
LightlessSync/Services/LightlessProfileData.cs
Normal file
7
LightlessSync/Services/LightlessProfileData.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public record LightlessProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Base64SupporterPicture, string Description)
|
||||
{
|
||||
public Lazy<byte[]> ImageData { get; } = new Lazy<byte[]>(Convert.FromBase64String(Base64ProfilePicture));
|
||||
public Lazy<byte[]> SupporterImageData { get; } = new Lazy<byte[]>(string.IsNullOrEmpty(Base64SupporterPicture) ? [] : Convert.FromBase64String(Base64SupporterPicture));
|
||||
}
|
||||
80
LightlessSync/Services/LightlessProfileManager.cs
Normal file
80
LightlessSync/Services/LightlessProfileManager.cs
Normal file
File diff suppressed because one or more lines are too long
@@ -6,12 +6,9 @@ using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Chat;
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.WebAPI.Files.Models;
|
||||
using System.Numerics;
|
||||
using LightlessSync.UI.Models;
|
||||
|
||||
namespace LightlessSync.Services.Mediator;
|
||||
|
||||
@@ -20,18 +17,14 @@ namespace LightlessSync.Services.Mediator;
|
||||
public record SwitchToIntroUiMessage : MessageBase;
|
||||
public record SwitchToMainUiMessage : MessageBase;
|
||||
public record OpenSettingsUiMessage : MessageBase;
|
||||
public record OpenLightfinderSettingsMessage : MessageBase;
|
||||
public record DalamudLoginMessage : MessageBase;
|
||||
public record DalamudLogoutMessage : MessageBase;
|
||||
public record ActorTrackedMessage(ActorObjectService.ActorDescriptor Descriptor) : SameThreadMessage;
|
||||
public record ActorUntrackedMessage(ActorObjectService.ActorDescriptor Descriptor) : SameThreadMessage;
|
||||
public record PriorityFrameworkUpdateMessage : SameThreadMessage;
|
||||
public record FrameworkUpdateMessage : SameThreadMessage;
|
||||
public record ClassJobChangedMessage(GameObjectHandler GameObjectHandler) : MessageBase;
|
||||
public record DelayedFrameworkUpdateMessage : SameThreadMessage;
|
||||
public record ZoneSwitchStartMessage : MessageBase;
|
||||
public record ZoneSwitchEndMessage : MessageBase;
|
||||
public record WorldChangedMessage(ushort PreviousWorldId, ushort CurrentWorldId) : MessageBase;
|
||||
public record CutsceneStartMessage : MessageBase;
|
||||
public record GposeStartMessage : SameThreadMessage;
|
||||
public record GposeEndMessage : MessageBase;
|
||||
@@ -54,43 +47,29 @@ public record PetNamesMessage(string PetNicknamesData) : MessageBase;
|
||||
public record HonorificReadyMessage : MessageBase;
|
||||
public record TransientResourceChangedMessage(IntPtr Address) : MessageBase;
|
||||
public record HaltScanMessage(string Source) : MessageBase;
|
||||
public record ResumeScanMessage(string Source) : MessageBase;
|
||||
public record NotificationMessage
|
||||
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
|
||||
public record PerformanceNotificationMessage
|
||||
(string Title, string Message, UserData UserData, bool IsPaused, string PlayerName) : MessageBase;
|
||||
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
|
||||
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
|
||||
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
|
||||
public record LightlessNotificationMessage(LightlessSync.UI.Models.LightlessNotification Notification) : MessageBase;
|
||||
public record LightlessNotificationDismissMessage(string NotificationId) : MessageBase;
|
||||
public record ClearAllNotificationsMessage : MessageBase;
|
||||
public record CharacterDataAnalyzedMessage : MessageBase;
|
||||
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;
|
||||
public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase;
|
||||
public record HubReconnectingMessage(Exception? Exception) : SameThreadMessage;
|
||||
public record HubReconnectedMessage(string? Arg) : SameThreadMessage;
|
||||
public record HubClosedMessage(Exception? Exception) : SameThreadMessage;
|
||||
public record ResumeScanMessage(string Source) : MessageBase;
|
||||
public record FileCacheInitializedMessage : MessageBase;
|
||||
public record DownloadReadyMessage(Guid RequestId) : MessageBase;
|
||||
public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
|
||||
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
|
||||
public record UiToggleMessage(Type UiType) : MessageBase;
|
||||
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase;
|
||||
public record ClearProfileUserDataMessage(UserData? UserData = null) : MessageBase;
|
||||
public record ClearProfileGroupDataMessage(GroupData? GroupData = null) : MessageBase;
|
||||
public record CyclePauseMessage(Pair Pair) : MessageBase;
|
||||
public record ClearProfileDataMessage(UserData? UserData = null) : MessageBase;
|
||||
public record CyclePauseMessage(UserData UserData) : MessageBase;
|
||||
public record PauseMessage(UserData UserData) : MessageBase;
|
||||
public record ProfilePopoutToggle(Pair? Pair) : MessageBase;
|
||||
public record CompactUiChange(Vector2 Size, Vector2 Position) : MessageBase;
|
||||
public record ProfileOpenStandaloneMessage(Pair Pair) : MessageBase;
|
||||
public record GroupProfileOpenStandaloneMessage(GroupData Group) : MessageBase;
|
||||
public record OpenGroupProfileEditorMessage(GroupFullInfoDto Group) : MessageBase;
|
||||
public record CloseGroupProfilePreviewMessage(GroupFullInfoDto Group) : MessageBase;
|
||||
public record ActiveServerChangedMessage(string ServerUrl) : MessageBase;
|
||||
public record OpenSelfProfilePreviewMessage(UserData User) : MessageBase;
|
||||
public record CloseSelfProfilePreviewMessage(UserData User) : MessageBase;
|
||||
public record OpenLightfinderProfileMessage(UserData User, LightlessProfileData ProfileData, string HashedCid) : MessageBase;
|
||||
public record RemoveWindowMessage(WindowMediatorSubscriberBase Window) : MessageBase;
|
||||
public record RefreshUiMessage : MessageBase;
|
||||
public record OpenBanUserPopupMessage(Pair PairToBan, GroupFullInfoDto GroupFullInfoDto) : MessageBase;
|
||||
@@ -99,11 +78,8 @@ public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase;
|
||||
public record OpenPermissionWindow(Pair Pair) : MessageBase;
|
||||
public record DownloadLimitChangedMessage() : SameThreadMessage;
|
||||
public record PairProcessingLimitChangedMessage : SameThreadMessage;
|
||||
public record PairDataChangedMessage : MessageBase;
|
||||
public record PairUiUpdatedMessage(PairUiSnapshot Snapshot) : MessageBase;
|
||||
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
||||
public record TargetPairMessage(Pair Pair) : MessageBase;
|
||||
public record PairFocusCharacterMessage(Pair Pair) : SameThreadMessage;
|
||||
public record CombatStartMessage : MessageBase;
|
||||
public record CombatEndMessage : MessageBase;
|
||||
public record PerformanceStartMessage : MessageBase;
|
||||
@@ -124,16 +100,8 @@ public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData)
|
||||
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
|
||||
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
|
||||
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
|
||||
public record UserLeftSyncshell(string gid) : MessageBase;
|
||||
public record UserJoinedSyncshell(string gid) : MessageBase;
|
||||
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
|
||||
public record PairRequestReceivedMessage(string HashedCid, string Message) : MessageBase;
|
||||
public record PairRequestsUpdatedMessage : MessageBase;
|
||||
public record PairDownloadStatusMessage(List<(string PlayerName, float Progress, string Status)> DownloadStatus, int QueueWaiting) : MessageBase;
|
||||
public record VisibilityChange : MessageBase;
|
||||
public record ChatChannelsUpdated : MessageBase;
|
||||
public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase;
|
||||
public record GroupCollectionChangedMessage : MessageBase;
|
||||
public record OpenUserProfileMessage(UserData User) : MessageBase;
|
||||
#pragma warning restore S2094
|
||||
#pragma warning restore MA0048 // File name must match type name
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using LightlessSync.UI.Style;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
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);
|
||||
|
||||
597
LightlessSync/Services/NameplateHandler.cs
Normal file
597
LightlessSync/Services/NameplateHandler.cs
Normal file
@@ -0,0 +1,597 @@
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.UtilsEnum.Enum;
|
||||
|
||||
// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you!
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
{
|
||||
private readonly ILogger<NameplateHandler> _logger;
|
||||
private readonly IAddonLifecycle _addonLifecycle;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly LightlessMediator _mediator;
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
private bool mEnabled = false;
|
||||
private bool _needsLabelRefresh = false;
|
||||
private AddonNamePlate* mpNameplateAddon = null;
|
||||
private readonly AtkTextNode*[] mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateTextWidths = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateTextHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateContainerHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateTextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
internal const uint mNameplateNodeIDBase = 0x7D99D500;
|
||||
private const string DefaultLabelText = "LightFinder";
|
||||
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
||||
private const int ContainerOffsetX = 50;
|
||||
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
||||
|
||||
private volatile HashSet<string> _activeBroadcastingCids = [];
|
||||
|
||||
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_addonLifecycle = addonLifecycle;
|
||||
_gameGui = gameGui;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_configService = configService;
|
||||
_mediator = mediator;
|
||||
_clientState = clientState;
|
||||
_pairManager = pairManager;
|
||||
|
||||
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||
}
|
||||
|
||||
internal void Init()
|
||||
{
|
||||
EnableNameplate();
|
||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
||||
}
|
||||
|
||||
internal void Uninit()
|
||||
{
|
||||
DisableNameplate();
|
||||
DestroyNameplateNodes();
|
||||
_mediator.Unsubscribe<PriorityFrameworkUpdateMessage>(this);
|
||||
mpNameplateAddon = null;
|
||||
}
|
||||
|
||||
internal void EnableNameplate()
|
||||
{
|
||||
if (!mEnabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
_addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour);
|
||||
mEnabled = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError($"Unknown error while trying to enable nameplate distances:\n{e}");
|
||||
DisableNameplate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void DisableNameplate()
|
||||
{
|
||||
if (mEnabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
_addonLifecycle.UnregisterListener(NameplateDrawDetour);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError($"Unknown error while unregistering nameplate listener:\n{e}");
|
||||
}
|
||||
|
||||
mEnabled = false;
|
||||
HideAllNameplateNodes();
|
||||
}
|
||||
}
|
||||
|
||||
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
|
||||
{
|
||||
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
||||
|
||||
if (mpNameplateAddon != pNameplateAddon)
|
||||
{
|
||||
for (int i = 0; i < mTextNodes.Length; ++i) mTextNodes[i] = null;
|
||||
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||
mpNameplateAddon = pNameplateAddon;
|
||||
if (mpNameplateAddon != null) CreateNameplateNodes();
|
||||
}
|
||||
|
||||
UpdateNameplateNodes();
|
||||
}
|
||||
|
||||
private void CreateNameplateNodes()
|
||||
{
|
||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
||||
{
|
||||
var nameplateObject = GetNameplateObject(i);
|
||||
if (nameplateObject == null)
|
||||
continue;
|
||||
|
||||
var pNameplateResNode = nameplateObject.Value.NameContainer;
|
||||
var pNewNode = AtkNodeHelpers.CreateOrphanTextNode(mNameplateNodeIDBase + (uint)i, TextFlags.Edge | TextFlags.Glare);
|
||||
|
||||
if (pNewNode != null)
|
||||
{
|
||||
var pLastChild = pNameplateResNode->ChildNode;
|
||||
while (pLastChild->PrevSiblingNode != null) pLastChild = pLastChild->PrevSiblingNode;
|
||||
pNewNode->AtkResNode.NextSiblingNode = pLastChild;
|
||||
pNewNode->AtkResNode.ParentNode = pNameplateResNode;
|
||||
pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode;
|
||||
nameplateObject.Value.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
|
||||
pNewNode->AtkResNode.SetUseDepthBasedPriority(true);
|
||||
mTextNodes[i] = pNewNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyNameplateNodes()
|
||||
{
|
||||
var pCurrentNameplateAddon = (AddonNamePlate*)_gameGui.GetAddonByName("NamePlate", 1).Address;
|
||||
if (mpNameplateAddon == null || mpNameplateAddon != pCurrentNameplateAddon)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
||||
{
|
||||
var pTextNode = mTextNodes[i];
|
||||
var pNameplateNode = GetNameplateComponentNode(i);
|
||||
if (pTextNode != null && pNameplateNode != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (pTextNode->AtkResNode.PrevSiblingNode != null)
|
||||
pTextNode->AtkResNode.PrevSiblingNode->NextSiblingNode = pTextNode->AtkResNode.NextSiblingNode;
|
||||
if (pTextNode->AtkResNode.NextSiblingNode != null)
|
||||
pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode;
|
||||
pNameplateNode->Component->UldManager.UpdateDrawNodeList();
|
||||
pTextNode->AtkResNode.Destroy(true);
|
||||
mTextNodes[i] = null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError($"Unknown error while removing text node 0x{(IntPtr)pTextNode:X} for nameplate {i} on component node 0x{(IntPtr)pNameplateNode:X}:\n{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||
}
|
||||
|
||||
private void HideAllNameplateNodes()
|
||||
{
|
||||
for (int i = 0; i < mTextNodes.Length; ++i)
|
||||
{
|
||||
HideNameplateTextNode(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateNameplateNodes()
|
||||
{
|
||||
var framework = Framework.Instance();
|
||||
var ui3DModule = framework->GetUIModule()->GetUI3DModule();
|
||||
|
||||
if (ui3DModule == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
||||
{
|
||||
var objectInfo = ui3DModule->NamePlateObjectInfoPointers[i].Value;
|
||||
|
||||
if (objectInfo == null || objectInfo->GameObject == null)
|
||||
continue;
|
||||
|
||||
var nameplateIndex = objectInfo->NamePlateIndex;
|
||||
if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
|
||||
continue;
|
||||
|
||||
var pNode = mTextNodes[nameplateIndex];
|
||||
if (pNode == null)
|
||||
continue;
|
||||
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject);
|
||||
|
||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_configService.Current.LightfinderLabelShowOwn && (objectInfo->GameObject->GetGameObjectId() == _clientState.LocalPlayer.GameObjectId))
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_configService.Current.LightfinderLabelShowPaired && VisibleUserIds.Any(u => u == objectInfo->GameObject->GetGameObjectId()))
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nameplateObject = mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
|
||||
nameplateObject.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
|
||||
|
||||
var pNameplateIconNode = nameplateObject.MarkerIcon;
|
||||
var pNameplateResNode = nameplateObject.NameContainer;
|
||||
var pNameplateTextNode = nameplateObject.NameText;
|
||||
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible());
|
||||
pNode->AtkResNode.ToggleVisibility(IsVisible);
|
||||
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
var nameText = nameplateObject.NameText;
|
||||
|
||||
if (nameContainer == null || nameText == null)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var labelColor = UIColors.Get("LightlessPurple");
|
||||
var edgeColor = UIColors.Get("FullBlack");
|
||||
var config = _configService.Current;
|
||||
|
||||
var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
|
||||
var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||
var effectiveScale = baseScale * scaleMultiplier;
|
||||
var labelContent = config.LightfinderLabelUseIcon
|
||||
? NormalizeIconGlyph(config.LightfinderLabelIconGlyph)
|
||||
: DefaultLabelText;
|
||||
|
||||
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
||||
pNode->AtkResNode.SetScale(effectiveScale, effectiveScale);
|
||||
var nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
if (nodeWidth <= 0)
|
||||
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||
var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f;
|
||||
var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
||||
pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255);
|
||||
AlignmentType alignment;
|
||||
|
||||
var textScaleY = nameText->AtkResNode.ScaleY;
|
||||
if (textScaleY <= 0f)
|
||||
textScaleY = 1f;
|
||||
|
||||
var blockHeight = System.Math.Abs((int)nameplateObject.TextH);
|
||||
if (blockHeight > 0)
|
||||
{
|
||||
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
blockHeight = _cachedNameplateTextHeights[nameplateIndex];
|
||||
}
|
||||
|
||||
if (blockHeight <= 0)
|
||||
{
|
||||
blockHeight = GetScaledTextHeight(nameText);
|
||||
if (blockHeight <= 0)
|
||||
blockHeight = nodeHeight;
|
||||
|
||||
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
||||
}
|
||||
|
||||
var containerHeight = (int)nameContainer->Height;
|
||||
if (containerHeight > 0)
|
||||
{
|
||||
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
containerHeight = _cachedNameplateContainerHeights[nameplateIndex];
|
||||
}
|
||||
|
||||
if (containerHeight <= 0)
|
||||
{
|
||||
containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY);
|
||||
if (containerHeight <= blockHeight)
|
||||
containerHeight = blockHeight + 1;
|
||||
|
||||
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
||||
}
|
||||
|
||||
var blockTop = containerHeight - blockHeight;
|
||||
if (blockTop < 0)
|
||||
blockTop = 0;
|
||||
var verticalPadding = (int)System.Math.Round(4 * effectiveScale);
|
||||
|
||||
var positionY = blockTop - verticalPadding - nodeHeight;
|
||||
|
||||
var textWidth = System.Math.Abs((int)nameplateObject.TextW);
|
||||
if (textWidth <= 0)
|
||||
{
|
||||
textWidth = GetScaledTextWidth(nameText);
|
||||
if (textWidth <= 0)
|
||||
textWidth = nodeWidth;
|
||||
}
|
||||
|
||||
if (textWidth > 0)
|
||||
{
|
||||
_cachedNameplateTextWidths[nameplateIndex] = textWidth;
|
||||
}
|
||||
|
||||
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
||||
var hasValidOffset = true;
|
||||
|
||||
if (System.Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0)
|
||||
{
|
||||
_cachedNameplateTextOffsets[nameplateIndex] = textOffset;
|
||||
}
|
||||
else if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue)
|
||||
{
|
||||
textOffset = _cachedNameplateTextOffsets[nameplateIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
hasValidOffset = false;
|
||||
}
|
||||
int positionX;
|
||||
|
||||
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
||||
{
|
||||
var nameplateWidth = (int)nameContainer->Width;
|
||||
|
||||
if (!config.LightfinderLabelUseIcon)
|
||||
{
|
||||
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
||||
pNode->AtkResNode.Width = 0;
|
||||
pNode->SetText(labelContent);
|
||||
|
||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
if (nodeWidth <= 0)
|
||||
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
|
||||
if (nodeWidth > nameplateWidth)
|
||||
nodeWidth = nameplateWidth;
|
||||
|
||||
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
|
||||
pNode->AtkResNode.Width = 0;
|
||||
pNode->SetText(labelContent);
|
||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
}
|
||||
|
||||
int leftPos = nameplateWidth / 8;
|
||||
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
||||
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
||||
int staticMargin = 24;
|
||||
int calcMargin = (int)(nameplateWidth * 0.08f);
|
||||
|
||||
switch (config.LabelAlignment)
|
||||
{
|
||||
case LabelAlignment.Left:
|
||||
positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos;
|
||||
alignment = AlignmentType.BottomLeft;
|
||||
break;
|
||||
case LabelAlignment.Right:
|
||||
positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin;
|
||||
alignment = AlignmentType.BottomRight;
|
||||
break;
|
||||
default:
|
||||
positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin;
|
||||
alignment = AlignmentType.Bottom;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
positionX = 58 + config.LightfinderLabelOffsetX;
|
||||
alignment = AlignmentType.Bottom;
|
||||
}
|
||||
|
||||
positionY += config.LightfinderLabelOffsetY;
|
||||
|
||||
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
||||
pNode->AtkResNode.SetUseDepthBasedPriority(true);
|
||||
|
||||
pNode->AtkResNode.Color.A = 255;
|
||||
|
||||
pNode->TextColor.R = (byte)(labelColor.X * 255);
|
||||
pNode->TextColor.G = (byte)(labelColor.Y * 255);
|
||||
pNode->TextColor.B = (byte)(labelColor.Z * 255);
|
||||
pNode->TextColor.A = (byte)(labelColor.W * 255);
|
||||
|
||||
pNode->EdgeColor.R = (byte)(edgeColor.X * 255);
|
||||
pNode->EdgeColor.G = (byte)(edgeColor.Y * 255);
|
||||
pNode->EdgeColor.B = (byte)(edgeColor.Z * 255);
|
||||
pNode->EdgeColor.A = (byte)(edgeColor.W * 255);
|
||||
|
||||
|
||||
if(!config.LightfinderLabelUseIcon)
|
||||
{
|
||||
pNode->AlignmentType = AlignmentType.Bottom;
|
||||
}
|
||||
else
|
||||
{
|
||||
pNode->AlignmentType = alignment;
|
||||
}
|
||||
pNode->AtkResNode.SetPositionShort(
|
||||
(short)System.Math.Clamp(positionX, short.MinValue, short.MaxValue),
|
||||
(short)System.Math.Clamp(positionY, short.MinValue, short.MaxValue)
|
||||
);
|
||||
var computedLineSpacing = (int)System.Math.Round(24 * scaleMultiplier);
|
||||
pNode->LineSpacing = (byte)System.Math.Clamp(computedLineSpacing, 0, byte.MaxValue);
|
||||
pNode->CharSpacing = 1;
|
||||
pNode->TextFlags = config.LightfinderLabelUseIcon
|
||||
? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize
|
||||
: TextFlags.Edge | TextFlags.Glare;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe int GetScaledTextHeight(AtkTextNode* node)
|
||||
{
|
||||
if (node == null)
|
||||
return 0;
|
||||
|
||||
var resNode = &node->AtkResNode;
|
||||
var rawHeight = (int)resNode->GetHeight();
|
||||
if (rawHeight <= 0 && node->LineSpacing > 0)
|
||||
rawHeight = node->LineSpacing;
|
||||
if (rawHeight <= 0)
|
||||
rawHeight = AtkNodeHelpers.DefaultTextNodeHeight;
|
||||
|
||||
var scale = resNode->ScaleY;
|
||||
if (scale <= 0f)
|
||||
scale = 1f;
|
||||
|
||||
var computed = (int)System.Math.Round(rawHeight * scale);
|
||||
return System.Math.Max(1, computed);
|
||||
}
|
||||
|
||||
private static unsafe int GetScaledTextWidth(AtkTextNode* node)
|
||||
{
|
||||
if (node == null)
|
||||
return 0;
|
||||
|
||||
var resNode = &node->AtkResNode;
|
||||
var rawWidth = (int)resNode->GetWidth();
|
||||
if (rawWidth <= 0)
|
||||
rawWidth = AtkNodeHelpers.DefaultTextNodeWidth;
|
||||
|
||||
var scale = resNode->ScaleX;
|
||||
if (scale <= 0f)
|
||||
scale = 1f;
|
||||
|
||||
var computed = (int)System.Math.Round(rawWidth * scale);
|
||||
return System.Math.Max(1, computed);
|
||||
}
|
||||
|
||||
internal static string NormalizeIconGlyph(string? rawInput)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawInput))
|
||||
return DefaultIconGlyph;
|
||||
|
||||
var trimmed = rawInput.Trim();
|
||||
|
||||
if (Enum.TryParse<SeIconChar>(trimmed, true, out var iconEnum))
|
||||
return SeIconCharExtensions.ToIconString(iconEnum);
|
||||
|
||||
var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? trimmed[2..]
|
||||
: trimmed;
|
||||
|
||||
if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue))
|
||||
return char.ConvertFromUtf32(hexValue);
|
||||
|
||||
var enumerator = trimmed.EnumerateRunes();
|
||||
if (enumerator.MoveNext())
|
||||
return enumerator.Current.ToString();
|
||||
|
||||
return DefaultIconGlyph;
|
||||
}
|
||||
|
||||
internal static string ToIconEditorString(string? rawInput)
|
||||
{
|
||||
var normalized = NormalizeIconGlyph(rawInput);
|
||||
var runeEnumerator = normalized.EnumerateRunes();
|
||||
return runeEnumerator.MoveNext()
|
||||
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
||||
: DefaultIconGlyph;
|
||||
}
|
||||
private void HideNameplateTextNode(int i)
|
||||
{
|
||||
var pNode = mTextNodes[i];
|
||||
if (pNode != null)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
private AddonNamePlate.NamePlateObject? GetNameplateObject(int i)
|
||||
{
|
||||
if (i < AddonNamePlate.NumNamePlateObjects &&
|
||||
mpNameplateAddon != null &&
|
||||
mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null)
|
||||
{
|
||||
return mpNameplateAddon->NamePlateObjectArray[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AtkComponentNode* GetNameplateComponentNode(int i)
|
||||
{
|
||||
var nameplateObject = GetNameplateObject(i);
|
||||
return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null;
|
||||
}
|
||||
private HashSet<ulong> VisibleUserIds => [.. _pairManager.GetOnlineUserPairs()
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||
|
||||
public void FlagRefresh()
|
||||
{
|
||||
_needsLabelRefresh = true;
|
||||
}
|
||||
|
||||
public void OnTick(PriorityFrameworkUpdateMessage _)
|
||||
{
|
||||
if (_needsLabelRefresh)
|
||||
{
|
||||
UpdateNameplateNodes();
|
||||
_needsLabelRefresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
||||
{
|
||||
var newSet = cids.ToHashSet();
|
||||
|
||||
var changed = !_activeBroadcastingCids.SetEquals(newSet);
|
||||
if (!changed)
|
||||
return;
|
||||
|
||||
_activeBroadcastingCids.Clear();
|
||||
foreach (var cid in newSet)
|
||||
_activeBroadcastingCids.Add(cid);
|
||||
|
||||
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(",", _activeBroadcastingCids));
|
||||
|
||||
FlagRefresh();
|
||||
}
|
||||
|
||||
public void ClearNameplateCaches()
|
||||
{
|
||||
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||
}
|
||||
}
|
||||
@@ -1,254 +1,114 @@
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.NativeWrapper;
|
||||
using Dalamud.Game.Gui.NamePlate;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.UI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using static LightlessSync.UI.DtrEntry;
|
||||
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
/// <summary>
|
||||
/// NameplateService is used for coloring our nameplates based on the settings of the user.
|
||||
/// </summary>
|
||||
public unsafe class NameplateService : DisposableMediatorSubscriberBase
|
||||
public class NameplateService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private delegate nint UpdateNameplateDelegate(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex);
|
||||
|
||||
// Glyceri, Thanks :bow:
|
||||
[Signature("40 53 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 84 24", DetourName = nameof(UpdateNameplateDetour))]
|
||||
private readonly Hook<UpdateNameplateDelegate>? _nameplateHook = null;
|
||||
|
||||
private readonly ILogger<NameplateService> _logger;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly INamePlateGui _namePlateGui;
|
||||
private readonly PairManager _pairManager;
|
||||
|
||||
public NameplateService(ILogger<NameplateService> logger,
|
||||
LightlessConfigService configService,
|
||||
INamePlateGui namePlateGui,
|
||||
IClientState clientState,
|
||||
IGameGui gameGui,
|
||||
IObjectTable objectTable,
|
||||
IGameInteropProvider interop,
|
||||
LightlessMediator lightlessMediator,
|
||||
PairUiService pairUiService) : base(logger, lightlessMediator)
|
||||
PairManager pairManager,
|
||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_namePlateGui = namePlateGui;
|
||||
_clientState = clientState;
|
||||
_gameGui = gameGui;
|
||||
_objectTable = objectTable;
|
||||
_pairUiService = pairUiService;
|
||||
_pairManager = pairManager;
|
||||
|
||||
interop.InitializeFromAttributes(this);
|
||||
_nameplateHook?.Enable();
|
||||
Refresh();
|
||||
|
||||
Mediator.Subscribe<VisibilityChange>(this, (_) => Refresh());
|
||||
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
||||
_namePlateGui.RequestRedraw();
|
||||
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detour for the game's internal nameplate update function.
|
||||
/// This will be called whenever the client updates any nameplate.
|
||||
///
|
||||
/// We hook into it to apply our own nameplate coloring logic via <see cref="SetNameplate"/>,
|
||||
/// </summary>
|
||||
private nint UpdateNameplateDetour(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
|
||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||
{
|
||||
try
|
||||
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)
|
||||
{
|
||||
SetNameplate(namePlateInfo, battleChara);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error in NameplateService UpdateNameplateDetour");
|
||||
}
|
||||
|
||||
return _nameplateHook!.Original(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the player should be colored based on conditions (isFriend, IsInParty)
|
||||
/// </summary>
|
||||
/// <param name="playerCharacter">Player character that will be checked</param>
|
||||
/// <param name="visibleUserIds">All visible users in the current object table</param>
|
||||
/// <returns>PLayer should or shouldnt be colored based on the result. True means colored</returns>
|
||||
private bool ShouldColorPlayer(IPlayerCharacter playerCharacter, HashSet<ulong> visibleUserIds)
|
||||
{
|
||||
if (!visibleUserIds.Contains(playerCharacter.GameObjectId))
|
||||
return false;
|
||||
|
||||
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 ((isInParty && !partyColorAllowed) || (isFriend && !friendColorAllowed))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setting up the nameplate of the user to be colored
|
||||
/// </summary>
|
||||
/// <param name="namePlateInfo">Information given from the Signature to be updated</param>
|
||||
/// <param name="battleChara">Character from FF</param>
|
||||
private void SetNameplate(RaptureAtkModule.NamePlateInfo* namePlateInfo, BattleChara* battleChara)
|
||||
{
|
||||
if (!_configService.Current.IsNameplateColorsEnabled || _clientState.IsPvPExcludingDen)
|
||||
return;
|
||||
if (namePlateInfo == null || battleChara == null)
|
||||
return;
|
||||
|
||||
var obj = _objectTable.FirstOrDefault(o => o.Address == (nint)battleChara);
|
||||
if (obj is not IPlayerCharacter player)
|
||||
return;
|
||||
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
var visibleUsersIds = snapshot.PairsByUid.Values
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)
|
||||
.ToHashSet();
|
||||
|
||||
//Check if player should be colored
|
||||
if (!ShouldColorPlayer(player, visibleUsersIds))
|
||||
return;
|
||||
|
||||
var originalName = player.Name.ToString();
|
||||
|
||||
//Check if not null of the name
|
||||
if (string.IsNullOrEmpty(originalName))
|
||||
return;
|
||||
|
||||
//Check if any characters/symbols are forbidden
|
||||
if (HasForbiddenSeStringChars(originalName))
|
||||
return;
|
||||
|
||||
//Swap color channels as we store them in BGR format as FF loves that
|
||||
var cfgColors = SwapColorChannels(_configService.Current.NameplateColors);
|
||||
var coloredName = WrapStringInColor(originalName, cfgColors.Glow, cfgColors.Foreground);
|
||||
|
||||
//Replace string of nameplate with our colored one
|
||||
namePlateInfo->Name.SetString(coloredName.EncodeWithNullTerminator());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts Uint code to Vector4 as we store Colors in Uint in our config, needed for lumina
|
||||
/// </summary>
|
||||
/// <param name="rgb">Color code</param>
|
||||
/// <returns>Vector4 Color</returns>
|
||||
private static Vector4 RgbUintToVector4(uint rgb)
|
||||
{
|
||||
float r = ((rgb >> 16) & 0xFF) / 255f;
|
||||
float g = ((rgb >> 8) & 0xFF) / 255f;
|
||||
float b = (rgb & 0xFF) / 255f;
|
||||
return new Vector4(r, g, b, 1f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the string has any forbidden characters/symbols as the string builder wouldnt append.
|
||||
/// </summary>
|
||||
/// <param name="s">String that has to be checked</param>
|
||||
/// <returns>Contains forbidden characters/symbols or not</returns>
|
||||
private static bool HasForbiddenSeStringChars(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s))
|
||||
return false;
|
||||
|
||||
foreach (var ch in s)
|
||||
{
|
||||
if (ch == '\0' || ch == '\u0002')
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps the given string with the given edge and text color.
|
||||
/// </summary>
|
||||
/// <param name="text">String that has to be wrapped</param>
|
||||
/// <param name="edgeColor">Edge(border) color</param>
|
||||
/// <param name="textColor">Text color</param>
|
||||
/// <returns>Color wrapped SeString</returns>
|
||||
public static SeString WrapStringInColor(string text, uint? edgeColor = null, uint? textColor = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return SeString.Empty;
|
||||
|
||||
var builder = new LSeStringBuilder();
|
||||
|
||||
if (textColor is uint tc)
|
||||
builder.PushColorRgba(RgbUintToVector4(tc));
|
||||
|
||||
if (edgeColor is uint ec)
|
||||
builder.PushEdgeColorRgba(RgbUintToVector4(ec));
|
||||
|
||||
builder.Append(text);
|
||||
|
||||
if (edgeColor != null)
|
||||
builder.PopEdgeColor();
|
||||
|
||||
if (textColor != null)
|
||||
builder.PopColor();
|
||||
|
||||
return builder.ToReadOnlySeString().ToDalamudString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request redraw of nameplates
|
||||
/// </summary>
|
||||
public void RequestRedraw()
|
||||
{
|
||||
Refresh();
|
||||
_namePlateGui.RequestRedraw();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the refresh of the Nameplate addon
|
||||
/// </summary>
|
||||
protected void Refresh()
|
||||
private static (SeString, SeString) CreateTextWrap(DtrEntry.Colors color)
|
||||
{
|
||||
AtkUnitBasePtr namePlateAddon = _gameGui.GetAddonByName("NamePlate");
|
||||
var left = new Lumina.Text.SeStringBuilder();
|
||||
var right = new Lumina.Text.SeStringBuilder();
|
||||
|
||||
if (namePlateAddon.IsNull)
|
||||
{
|
||||
_logger.LogInformation("NamePlate addon is null, cannot refresh nameplates.");
|
||||
return;
|
||||
}
|
||||
left.PushColorRgba(color.Foreground);
|
||||
right.PopColor();
|
||||
|
||||
var addonNamePlate = (AddonNamePlate*)namePlateAddon.Address;
|
||||
left.PushEdgeColorRgba(color.Glow);
|
||||
right.PopEdgeColor();
|
||||
|
||||
if (addonNamePlate == null)
|
||||
{
|
||||
_logger.LogInformation("addonNamePlate addon is null, cannot refresh nameplates.");
|
||||
return;
|
||||
}
|
||||
|
||||
addonNamePlate->DoFullUpdate = 1;
|
||||
return (left.ToReadOnlySeString().ToDalamudString(), right.ToReadOnlySeString().ToDalamudString());
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_nameplateHook?.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
|
||||
_namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate;
|
||||
_namePlateGui.RequestRedraw();
|
||||
}
|
||||
}
|
||||
@@ -1,460 +1,102 @@
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Models;
|
||||
using LightlessSync.UI.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using LightlessSync.API.Data;
|
||||
using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public class NotificationService : DisposableMediatorSubscriberBase, IHostedService
|
||||
{
|
||||
private readonly ILogger<NotificationService> _logger;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly INotificationManager _notificationManager;
|
||||
private readonly IChatGui _chatGui;
|
||||
private readonly PairRequestService _pairRequestService;
|
||||
private readonly HashSet<string> _shownPairRequestNotifications = [];
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly PairFactory _pairFactory;
|
||||
private readonly LightlessConfigService _configurationService;
|
||||
|
||||
public NotificationService(
|
||||
ILogger<NotificationService> logger,
|
||||
LightlessConfigService configService,
|
||||
public NotificationService(ILogger<NotificationService> logger, LightlessMediator mediator,
|
||||
DalamudUtilService dalamudUtilService,
|
||||
INotificationManager notificationManager,
|
||||
IChatGui chatGui,
|
||||
LightlessMediator mediator,
|
||||
PairRequestService pairRequestService,
|
||||
PairUiService pairUiService,
|
||||
PairFactory pairFactory) : base(logger, mediator)
|
||||
IChatGui chatGui, LightlessConfigService configurationService) : base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_notificationManager = notificationManager;
|
||||
_chatGui = chatGui;
|
||||
_pairRequestService = pairRequestService;
|
||||
_pairUiService = pairUiService;
|
||||
_pairFactory = pairFactory;
|
||||
_configurationService = configurationService;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
|
||||
Mediator.Subscribe<PairRequestReceivedMessage>(this, HandlePairRequestReceived);
|
||||
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
|
||||
Mediator.Subscribe<PairDownloadStatusMessage>(this, HandlePairDownloadStatus);
|
||||
Mediator.Subscribe<PerformanceNotificationMessage>(this, HandlePerformanceNotification);
|
||||
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
|
||||
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var notification = CreateNotification(title, message, type, duration, actions, soundEffectId);
|
||||
|
||||
if (_configService.Current.AutoDismissOnAction && notification.Actions.Count != 0)
|
||||
{
|
||||
WrapActionsWithAutoDismiss(notification);
|
||||
}
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private LightlessNotification CreateNotification(string title, string message, NotificationType type,
|
||||
TimeSpan? duration, List<LightlessNotificationAction>? actions, uint? soundEffectId)
|
||||
private void PrintErrorChat(string? message)
|
||||
{
|
||||
return new LightlessNotification
|
||||
{
|
||||
Title = title,
|
||||
Message = message,
|
||||
Type = type,
|
||||
Duration = duration ?? GetDefaultDurationForType(type),
|
||||
Actions = actions ?? new List<LightlessNotificationAction>(),
|
||||
SoundEffectId = GetSoundEffectId(type, soundEffectId),
|
||||
ShowProgress = _configService.Current.ShowNotificationProgress,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
|
||||
_chatGui.PrintError(se.BuiltString);
|
||||
}
|
||||
|
||||
private void WrapActionsWithAutoDismiss(LightlessNotification notification)
|
||||
private void PrintInfoChat(string? message)
|
||||
{
|
||||
foreach (var action in notification.Actions)
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty);
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
private void PrintWarnChat(string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
private void ShowChat(NotificationMessage msg)
|
||||
{
|
||||
switch (msg.Type)
|
||||
{
|
||||
var originalOnClick = action.OnClick;
|
||||
action.OnClick = (n) =>
|
||||
{
|
||||
originalOnClick(n);
|
||||
if (_configService.Current.AutoDismissOnAction)
|
||||
{
|
||||
DismissNotification(n);
|
||||
}
|
||||
};
|
||||
case NotificationType.Info:
|
||||
PrintInfoChat(msg.Message);
|
||||
break;
|
||||
|
||||
case NotificationType.Warning:
|
||||
PrintWarnChat(msg.Message);
|
||||
break;
|
||||
|
||||
case NotificationType.Error:
|
||||
PrintErrorChat(msg.Message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void DismissNotification(LightlessNotification notification)
|
||||
private void ShowNotification(NotificationMessage msg)
|
||||
{
|
||||
notification.IsDismissed = true;
|
||||
notification.IsAnimatingOut = true;
|
||||
}
|
||||
Logger.LogInformation("{msg}", msg.ToString());
|
||||
|
||||
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
|
||||
{
|
||||
var location = GetNotificationLocation(NotificationType.PairRequest);
|
||||
|
||||
// Show in chat if configured
|
||||
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
|
||||
{
|
||||
ShowChat(new NotificationMessage("Pair Request Received", $"{senderName} wants to directly pair with you.", NotificationType.PairRequest));
|
||||
}
|
||||
|
||||
// Show Lightless notification if configured and action buttons are enabled
|
||||
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
|
||||
&& _configService.Current.UseLightlessNotifications
|
||||
&& _configService.Current.ShowPairRequestNotificationActions)
|
||||
{
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Id = $"pair_request_{senderId}",
|
||||
Title = "Pair Request Received",
|
||||
Message = $"{senderName} wants to directly pair with you.",
|
||||
Type = NotificationType.PairRequest,
|
||||
Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
||||
SoundEffectId = GetPairRequestSoundId(),
|
||||
Actions = CreatePairRequestActions(onAccept, onDecline)
|
||||
};
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
|
||||
{
|
||||
// Fall back to regular notification without action buttons
|
||||
HandleNotificationMessage(new NotificationMessage("Pair Request Received", $"{senderName} wants to directly pair with you.", NotificationType.PairRequest));
|
||||
}
|
||||
}
|
||||
|
||||
private uint? GetPairRequestSoundId() =>
|
||||
!_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null;
|
||||
|
||||
private List<LightlessNotificationAction> CreatePairRequestActions(Action onAccept, Action onDecline)
|
||||
{
|
||||
return new List<LightlessNotificationAction>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = "accept",
|
||||
Label = "Accept",
|
||||
Icon = FontAwesomeIcon.Check,
|
||||
Color = UIColors.Get("LightlessGreen"),
|
||||
IsPrimary = true,
|
||||
OnClick = (n) =>
|
||||
{
|
||||
_logger.LogInformation("Pair request accepted");
|
||||
onAccept();
|
||||
DismissNotification(n);
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "decline",
|
||||
Label = "Decline",
|
||||
Icon = FontAwesomeIcon.Times,
|
||||
Color = UIColors.Get("DimRed"),
|
||||
IsDestructive = true,
|
||||
OnClick = (n) =>
|
||||
{
|
||||
_logger.LogInformation("Pair request declined");
|
||||
onDecline();
|
||||
DismissNotification(n);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null)
|
||||
{
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Title = "Download Complete",
|
||||
Message = FormatDownloadCompleteMessage(fileName, fileCount),
|
||||
Type = NotificationType.Info,
|
||||
Duration = TimeSpan.FromSeconds(8),
|
||||
Actions = CreateDownloadCompleteActions(onOpenFolder),
|
||||
SoundEffectId = NotificationSounds.DownloadComplete
|
||||
};
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
|
||||
private static string FormatDownloadCompleteMessage(string fileName, int fileCount)
|
||||
{
|
||||
return fileCount > 1
|
||||
? $"Downloaded {fileCount} files successfully."
|
||||
: $"Downloaded {fileName} successfully.";
|
||||
}
|
||||
|
||||
private List<LightlessNotificationAction> CreateDownloadCompleteActions(Action? onOpenFolder)
|
||||
{
|
||||
var actions = new List<LightlessNotificationAction>();
|
||||
|
||||
if (onOpenFolder != null)
|
||||
{
|
||||
actions.Add(new LightlessNotificationAction
|
||||
{
|
||||
Id = "open_folder",
|
||||
Label = "Open Folder",
|
||||
Icon = FontAwesomeIcon.FolderOpen,
|
||||
Color = UIColors.Get("LightlessBlue"),
|
||||
OnClick = (n) =>
|
||||
{
|
||||
onOpenFolder();
|
||||
DismissNotification(n);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null,
|
||||
Action? onViewLog = null)
|
||||
{
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Title = title,
|
||||
Message = FormatErrorMessage(message, exception),
|
||||
Type = NotificationType.Error,
|
||||
Duration = TimeSpan.FromSeconds(15),
|
||||
Actions = CreateErrorActions(onRetry, onViewLog),
|
||||
SoundEffectId = NotificationSounds.Error
|
||||
};
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
|
||||
private static string FormatErrorMessage(string message, Exception? exception)
|
||||
{
|
||||
return exception != null ? $"{message}\n\nError: {exception.Message}" : message;
|
||||
}
|
||||
|
||||
private List<LightlessNotificationAction> CreateErrorActions(Action? onRetry, Action? onViewLog)
|
||||
{
|
||||
var actions = new List<LightlessNotificationAction>();
|
||||
|
||||
if (onRetry != null)
|
||||
{
|
||||
actions.Add(new LightlessNotificationAction
|
||||
{
|
||||
Id = "retry",
|
||||
Label = "Retry",
|
||||
Icon = FontAwesomeIcon.Redo,
|
||||
Color = UIColors.Get("LightlessBlue"),
|
||||
OnClick = (n) =>
|
||||
{
|
||||
onRetry();
|
||||
DismissNotification(n);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (onViewLog != null)
|
||||
{
|
||||
actions.Add(new LightlessNotificationAction
|
||||
{
|
||||
Id = "view_log",
|
||||
Label = "View Log",
|
||||
Icon = FontAwesomeIcon.FileAlt,
|
||||
Color = UIColors.Get("LightlessYellow"),
|
||||
OnClick = (n) => onViewLog()
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
|
||||
private string BuildPairDownloadMessage(List<(string PlayerName, float Progress, string Status)> userDownloads,
|
||||
int queueWaiting)
|
||||
{
|
||||
var messageParts = new List<string>();
|
||||
|
||||
if (queueWaiting > 0)
|
||||
{
|
||||
messageParts.Add($"Queue: {queueWaiting} waiting");
|
||||
}
|
||||
|
||||
if (userDownloads.Count > 0)
|
||||
{
|
||||
var completedCount = userDownloads.Count(x => x.Progress >= 1.0f);
|
||||
messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed");
|
||||
}
|
||||
|
||||
var activeDownloadLines = BuildActiveDownloadLines(userDownloads);
|
||||
if (!string.IsNullOrEmpty(activeDownloadLines))
|
||||
{
|
||||
messageParts.Add(activeDownloadLines);
|
||||
}
|
||||
|
||||
return string.Join("\n", messageParts);
|
||||
}
|
||||
|
||||
private string BuildActiveDownloadLines(List<(string PlayerName, float Progress, string Status)> userDownloads)
|
||||
{
|
||||
var activeDownloads = userDownloads
|
||||
.Where(x => x.Progress < 1.0f)
|
||||
.Take(_configService.Current.MaxConcurrentPairApplications);
|
||||
|
||||
if (!activeDownloads.Any()) return string.Empty;
|
||||
|
||||
return string.Join("\n", activeDownloads.Select(x => $"• {x.PlayerName}: {FormatDownloadStatus(x)}"));
|
||||
}
|
||||
|
||||
private static string FormatDownloadStatus((string PlayerName, float Progress, string Status) download)
|
||||
{
|
||||
return download.Status switch
|
||||
{
|
||||
"downloading" => $"{download.Progress:P0}",
|
||||
"decompressing" => "decompressing",
|
||||
"queued" => "queued",
|
||||
"waiting" => "waiting for slot",
|
||||
_ => download.Status
|
||||
};
|
||||
}
|
||||
|
||||
private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => TimeSpan.FromSeconds(_configService.Current.InfoNotificationDurationSeconds),
|
||||
NotificationType.Warning => TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds),
|
||||
NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds),
|
||||
NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
||||
NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
|
||||
NotificationType.Performance => TimeSpan.FromSeconds(_configService.Current.PerformanceNotificationDurationSeconds),
|
||||
_ => TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId)
|
||||
{
|
||||
if (overrideSoundId.HasValue) return overrideSoundId;
|
||||
if (IsSoundDisabledForType(type)) return null;
|
||||
return GetConfiguredSoundForType(type);
|
||||
}
|
||||
|
||||
private bool IsSoundDisabledForType(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => _configService.Current.DisableInfoSound,
|
||||
NotificationType.Warning => _configService.Current.DisableWarningSound,
|
||||
NotificationType.Error => _configService.Current.DisableErrorSound,
|
||||
NotificationType.Performance => _configService.Current.DisablePerformanceSound,
|
||||
NotificationType.Download => true, // Download sounds always disabled
|
||||
_ => false
|
||||
};
|
||||
|
||||
private uint GetConfiguredSoundForType(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => _configService.Current.CustomInfoSoundId,
|
||||
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
|
||||
NotificationType.Error => _configService.Current.CustomErrorSoundId,
|
||||
NotificationType.Performance => _configService.Current.PerformanceSoundId,
|
||||
_ => NotificationSounds.GetDefaultSound(type)
|
||||
};
|
||||
|
||||
private void PlayNotificationSound(uint soundEffectId)
|
||||
{
|
||||
try
|
||||
{
|
||||
UIGlobals.PlayChatSoundEffect(soundEffectId);
|
||||
_logger.LogDebug("Played notification sound effect {SoundId} via ChatGui", soundEffectId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to play notification sound effect {SoundId}", soundEffectId);
|
||||
}
|
||||
}
|
||||
private Pair? ResolvePair(UserData userData)
|
||||
{
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
if (snapshot.PairsByUid.TryGetValue(userData.UID, out var pair))
|
||||
{
|
||||
return pair;
|
||||
}
|
||||
|
||||
var ident = new PairUniqueIdentifier(userData.UID);
|
||||
return _pairFactory.Create(ident);
|
||||
}
|
||||
|
||||
private void HandleNotificationMessage(NotificationMessage msg)
|
||||
{
|
||||
_logger.LogInformation("{msg}", msg.ToString());
|
||||
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||
|
||||
var location = GetNotificationLocation(msg.Type);
|
||||
ShowNotificationLocationBased(msg, location);
|
||||
switch (msg.Type)
|
||||
{
|
||||
case NotificationType.Info:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
|
||||
break;
|
||||
|
||||
case NotificationType.Warning:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
|
||||
break;
|
||||
|
||||
case NotificationType.Error:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationLocation GetNotificationLocation(NotificationType type) =>
|
||||
_configService.Current.UseLightlessNotifications
|
||||
? GetLightlessNotificationLocation(type)
|
||||
: GetClassicNotificationLocation(type);
|
||||
|
||||
private NotificationLocation GetLightlessNotificationLocation(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => _configService.Current.LightlessInfoNotification,
|
||||
NotificationType.Warning => _configService.Current.LightlessWarningNotification,
|
||||
NotificationType.Error => _configService.Current.LightlessErrorNotification,
|
||||
NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification,
|
||||
NotificationType.Download => _configService.Current.LightlessDownloadNotification,
|
||||
NotificationType.Performance => _configService.Current.LightlessPerformanceNotification,
|
||||
_ => NotificationLocation.LightlessUi
|
||||
};
|
||||
|
||||
private NotificationLocation GetClassicNotificationLocation(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => _configService.Current.InfoNotification,
|
||||
NotificationType.Warning => _configService.Current.WarningNotification,
|
||||
NotificationType.Error => _configService.Current.ErrorNotification,
|
||||
NotificationType.PairRequest => NotificationLocation.Toast,
|
||||
NotificationType.Download => NotificationLocation.Toast,
|
||||
_ => NotificationLocation.Nowhere
|
||||
};
|
||||
|
||||
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
|
||||
{
|
||||
switch (location)
|
||||
@@ -472,29 +114,20 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
||||
ShowChat(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.LightlessUi:
|
||||
ShowLightlessNotification(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.ChatAndLightlessUi:
|
||||
ShowChat(msg);
|
||||
ShowLightlessNotification(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Nowhere:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowLightlessNotification(NotificationMessage msg)
|
||||
{
|
||||
var duration = msg.TimeShownOnScreen ?? GetDefaultDurationForType(msg.Type);
|
||||
ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null);
|
||||
}
|
||||
|
||||
private void ShowToast(NotificationMessage msg)
|
||||
{
|
||||
var dalamudType = ConvertToDalamudNotificationType(msg.Type);
|
||||
Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch
|
||||
{
|
||||
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
|
||||
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||
NotificationType.Info => Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
||||
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
|
||||
};
|
||||
|
||||
_notificationManager.AddNotification(new Notification()
|
||||
{
|
||||
@@ -505,279 +138,4 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
||||
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
|
||||
});
|
||||
}
|
||||
|
||||
private static Dalamud.Interface.ImGuiNotification.NotificationType
|
||||
ConvertToDalamudNotificationType(NotificationType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
|
||||
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
|
||||
};
|
||||
}
|
||||
|
||||
private void ShowChat(NotificationMessage msg)
|
||||
{
|
||||
switch (msg.Type)
|
||||
{
|
||||
case NotificationType.Info:
|
||||
PrintInfoChat(msg.Message);
|
||||
break;
|
||||
|
||||
case NotificationType.Warning:
|
||||
PrintWarnChat(msg.Message);
|
||||
break;
|
||||
|
||||
case NotificationType.Error:
|
||||
PrintErrorChat(msg.Message);
|
||||
break;
|
||||
|
||||
case NotificationType.PairRequest:
|
||||
PrintPairRequestChat(msg.Title, msg.Message);
|
||||
break;
|
||||
|
||||
case NotificationType.Performance:
|
||||
PrintPerformanceChat(msg.Title, msg.Message);
|
||||
break;
|
||||
|
||||
// Download notifications don't support chat output, will be a giga spam otherwise
|
||||
case NotificationType.Download:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintErrorChat(string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
|
||||
_chatGui.PrintError(se.BuiltString);
|
||||
}
|
||||
|
||||
private void PrintInfoChat(string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ")
|
||||
.AddItalics(message ?? string.Empty);
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
private void PrintWarnChat(string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
|
||||
.AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
private void PrintPairRequestChat(string? title, string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
|
||||
.AddUiForeground("Pair Request: ", 541).AddUiForegroundOff()
|
||||
.AddText(title ?? message ?? string.Empty);
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
private void PrintPerformanceChat(string? title, string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
|
||||
.AddUiForeground("Performance: ", 508).AddUiForegroundOff()
|
||||
.AddText(title ?? message ?? string.Empty);
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
private void HandlePairRequestReceived(PairRequestReceivedMessage msg)
|
||||
{
|
||||
var request = _pairRequestService.RegisterIncomingRequest(msg.HashedCid, msg.Message);
|
||||
var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName;
|
||||
|
||||
_shownPairRequestNotifications.Add(request.HashedCid);
|
||||
ShowPairRequestNotification(
|
||||
senderName,
|
||||
request.HashedCid,
|
||||
onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid, senderName),
|
||||
onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid, senderName));
|
||||
}
|
||||
|
||||
private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
|
||||
{
|
||||
var activeRequests = _pairRequestService.GetActiveRequests();
|
||||
var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
// Dismiss notifications for requests that are no longer active (expired)
|
||||
var notificationsToRemove = _shownPairRequestNotifications
|
||||
.Where(hashedCid => !activeRequestIds.Contains(hashedCid))
|
||||
.ToList();
|
||||
|
||||
foreach (var hashedCid in notificationsToRemove)
|
||||
{
|
||||
var notificationId = $"pair_request_{hashedCid}";
|
||||
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId));
|
||||
_shownPairRequestNotifications.Remove(hashedCid);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePairDownloadStatus(PairDownloadStatusMessage msg)
|
||||
{
|
||||
var userDownloads = msg.DownloadStatus.Where(x => !string.Equals(x.PlayerName, "Pair Queue", StringComparison.Ordinal)).ToList();
|
||||
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.Progress) : 0f;
|
||||
var message = BuildPairDownloadMessage(userDownloads, msg.QueueWaiting);
|
||||
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Id = "pair_download_progress",
|
||||
Title = "Downloading Pair Data",
|
||||
Message = message,
|
||||
Type = NotificationType.Download,
|
||||
Duration = TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
|
||||
ShowProgress = true,
|
||||
Progress = totalProgress
|
||||
};
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
|
||||
private void HandlePerformanceNotification(PerformanceNotificationMessage msg)
|
||||
{
|
||||
var location = GetNotificationLocation(NotificationType.Performance);
|
||||
|
||||
// Show in chat if configured
|
||||
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
|
||||
{
|
||||
ShowChat(new NotificationMessage(msg.Title, msg.Message, NotificationType.Performance));
|
||||
}
|
||||
|
||||
// Show Lightless notification if configured and action buttons are enabled
|
||||
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
|
||||
&& _configService.Current.UseLightlessNotifications
|
||||
&& _configService.Current.ShowPerformanceNotificationActions)
|
||||
{
|
||||
var actions = CreatePerformanceActions(msg.UserData, msg.IsPaused, msg.PlayerName);
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Title = msg.Title,
|
||||
Message = msg.Message,
|
||||
Type = NotificationType.Performance,
|
||||
Duration = TimeSpan.FromSeconds(_configService.Current.PerformanceNotificationDurationSeconds),
|
||||
Actions = actions,
|
||||
SoundEffectId = GetSoundEffectId(NotificationType.Performance, null)
|
||||
};
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
|
||||
{
|
||||
// Fall back to regular notification without action buttons
|
||||
HandleNotificationMessage(new NotificationMessage(msg.Title, msg.Message, NotificationType.Performance));
|
||||
}
|
||||
}
|
||||
|
||||
private List<LightlessNotificationAction> CreatePerformanceActions(UserData userData, bool isPaused, string playerName)
|
||||
{
|
||||
var actions = new List<LightlessNotificationAction>();
|
||||
|
||||
if (isPaused)
|
||||
{
|
||||
actions.Add(new LightlessNotificationAction
|
||||
{
|
||||
Label = "Unpause",
|
||||
Icon = FontAwesomeIcon.Play,
|
||||
Color = UIColors.Get("LightlessGreen"),
|
||||
IsPrimary = true,
|
||||
OnClick = (notification) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var pair = ResolvePair(userData);
|
||||
if (pair == null)
|
||||
{
|
||||
_logger.LogWarning("Cannot cycle pause {uid} because pair is missing", userData.UID);
|
||||
throw new InvalidOperationException("Pair not available");
|
||||
}
|
||||
|
||||
Mediator.Publish(new CyclePauseMessage(pair));
|
||||
DismissNotification(notification);
|
||||
|
||||
var displayName = GetUserDisplayName(userData, playerName);
|
||||
ShowNotification(
|
||||
"Player Unpaused",
|
||||
$"Successfully unpaused {displayName}",
|
||||
NotificationType.Info,
|
||||
TimeSpan.FromSeconds(3));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to unpause player {uid}", userData.UID);
|
||||
var displayName = GetUserDisplayName(userData, playerName);
|
||||
ShowNotification(
|
||||
"Unpause Failed",
|
||||
$"Failed to unpause {displayName}",
|
||||
NotificationType.Error,
|
||||
TimeSpan.FromSeconds(5));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
actions.Add(new LightlessNotificationAction
|
||||
{
|
||||
Label = "Pause",
|
||||
Icon = FontAwesomeIcon.Pause,
|
||||
Color = UIColors.Get("LightlessOrange"),
|
||||
IsPrimary = true,
|
||||
OnClick = (notification) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Mediator.Publish(new PauseMessage(userData));
|
||||
DismissNotification(notification);
|
||||
|
||||
var displayName = GetUserDisplayName(userData, playerName);
|
||||
ShowNotification(
|
||||
"Player Paused",
|
||||
$"Successfully paused {displayName}",
|
||||
NotificationType.Info,
|
||||
TimeSpan.FromSeconds(3));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to pause player {uid}", userData.UID);
|
||||
var displayName = GetUserDisplayName(userData, playerName);
|
||||
ShowNotification(
|
||||
"Pause Failed",
|
||||
$"Failed to pause {displayName}",
|
||||
NotificationType.Error,
|
||||
TimeSpan.FromSeconds(5));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add dismiss button
|
||||
actions.Add(new LightlessNotificationAction
|
||||
{
|
||||
Label = "Dismiss",
|
||||
Icon = FontAwesomeIcon.Times,
|
||||
Color = UIColors.Get("DimRed"),
|
||||
IsPrimary = false,
|
||||
OnClick = (notification) =>
|
||||
{
|
||||
DismissNotification(notification);
|
||||
}
|
||||
});
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
private static string GetUserDisplayName(UserData userData, string playerName)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(userData.Alias) && !string.Equals(userData.Alias, userData.UID, StringComparison.Ordinal))
|
||||
{
|
||||
return $"{playerName} ({userData.Alias})";
|
||||
}
|
||||
return $"{playerName} ({userData.UID})";
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LightlessSync.Services.PairProcessing;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly record struct PairProcessingLimiterSnapshot(bool IsEnabled, int Limit, int InFlight, int Waiting)
|
||||
{
|
||||
public int Remaining => Math.Max(0, Limit - InFlight);
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services.PairProcessing;
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private const int _hardLimit = 32;
|
||||
private const int HardLimit = 32;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly object _limitLock = new();
|
||||
private readonly SemaphoreSlim _semaphore;
|
||||
private int _currentLimit;
|
||||
private int _pendingReductions;
|
||||
private int _pendingIncrements;
|
||||
private int _waiting;
|
||||
private int _inFlight;
|
||||
|
||||
@@ -21,8 +23,8 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
||||
{
|
||||
_configService = configService;
|
||||
_currentLimit = CalculateLimit();
|
||||
var initialCount = _configService.Current.EnablePairProcessingLimiter ? _currentLimit : _hardLimit;
|
||||
_semaphore = new SemaphoreSlim(initialCount, _hardLimit);
|
||||
var initialCount = _configService.Current.EnablePairProcessingLimiter ? _currentLimit : HardLimit;
|
||||
_semaphore = new SemaphoreSlim(initialCount, HardLimit);
|
||||
|
||||
Mediator.Subscribe<PairProcessingLimitChangedMessage>(this, _ => UpdateSemaphoreLimit());
|
||||
}
|
||||
@@ -68,7 +70,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
||||
|
||||
if (!IsEnabled)
|
||||
{
|
||||
TryReleaseSemaphore();
|
||||
_semaphore.Release();
|
||||
return NoopReleaser.Instance;
|
||||
}
|
||||
|
||||
@@ -85,15 +87,21 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
var releaseAmount = _hardLimit - _semaphore.CurrentCount;
|
||||
var releaseAmount = HardLimit - _semaphore.CurrentCount;
|
||||
if (releaseAmount > 0)
|
||||
{
|
||||
TryReleaseSemaphore(releaseAmount);
|
||||
try
|
||||
{
|
||||
_semaphore.Release(releaseAmount);
|
||||
}
|
||||
catch (SemaphoreFullException)
|
||||
{
|
||||
// ignore, already at max
|
||||
}
|
||||
}
|
||||
|
||||
_currentLimit = desiredLimit;
|
||||
_pendingReductions = 0;
|
||||
_pendingIncrements = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,13 +113,10 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
||||
if (desiredLimit > _currentLimit)
|
||||
{
|
||||
var increment = desiredLimit - _currentLimit;
|
||||
_pendingIncrements += increment;
|
||||
|
||||
var available = _hardLimit - _semaphore.CurrentCount;
|
||||
var toRelease = Math.Min(_pendingIncrements, available);
|
||||
if (toRelease > 0 && TryReleaseSemaphore(toRelease))
|
||||
var allowed = Math.Min(increment, HardLimit - _semaphore.CurrentCount);
|
||||
if (allowed > 0)
|
||||
{
|
||||
_pendingIncrements -= toRelease;
|
||||
_semaphore.Release(allowed);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -128,13 +133,6 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
||||
{
|
||||
_pendingReductions += remaining;
|
||||
}
|
||||
|
||||
if (_pendingIncrements > 0)
|
||||
{
|
||||
var offset = Math.Min(_pendingIncrements, _pendingReductions);
|
||||
_pendingIncrements -= offset;
|
||||
_pendingReductions -= offset;
|
||||
}
|
||||
}
|
||||
|
||||
_currentLimit = desiredLimit;
|
||||
@@ -145,26 +143,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
||||
private int CalculateLimit()
|
||||
{
|
||||
var configured = _configService.Current.MaxConcurrentPairApplications;
|
||||
return Math.Clamp(configured, 1, _hardLimit);
|
||||
}
|
||||
|
||||
private bool TryReleaseSemaphore(int count = 1)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_semaphore.Release(count);
|
||||
return true;
|
||||
}
|
||||
catch (SemaphoreFullException ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Attempted to release {count} pair processing slots but semaphore is already at the hard limit.", count);
|
||||
return false;
|
||||
}
|
||||
return Math.Clamp(configured, 1, HardLimit);
|
||||
}
|
||||
|
||||
private void ReleaseOne()
|
||||
@@ -187,20 +166,9 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
||||
_pendingReductions--;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pendingIncrements > 0)
|
||||
{
|
||||
if (!TryReleaseSemaphore())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingIncrements--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TryReleaseSemaphore();
|
||||
_semaphore.Release();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -245,3 +213,8 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct PairProcessingLimiterSnapshot(bool IsEnabled, int Limit, int InFlight, int Waiting)
|
||||
{
|
||||
public int Remaining => Math.Max(0, Limit - InFlight);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
@@ -8,25 +10,17 @@ namespace LightlessSync.Services;
|
||||
public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly Lazy<WebAPI.ApiController> _apiController;
|
||||
private readonly Lock _syncRoot = new();
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly object _syncRoot = new();
|
||||
private readonly List<PairRequestEntry> _requests = [];
|
||||
private readonly Dictionary<string, string> _displayNameCache = new(StringComparer.Ordinal);
|
||||
|
||||
private static readonly TimeSpan _expiration = TimeSpan.FromMinutes(5);
|
||||
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);
|
||||
|
||||
public PairRequestService(
|
||||
ILogger<PairRequestService> logger,
|
||||
LightlessMediator mediator,
|
||||
DalamudUtilService dalamudUtil,
|
||||
PairUiService pairUiService,
|
||||
Lazy<WebAPI.ApiController> apiController)
|
||||
public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pairUiService = pairUiService;
|
||||
_apiController = apiController;
|
||||
_pairManager = pairManager;
|
||||
|
||||
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ =>
|
||||
{
|
||||
@@ -97,10 +91,6 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
||||
lock (_syncRoot)
|
||||
{
|
||||
removed = _requests.RemoveAll(r => string.Equals(r.HashedCid, hashedCid, StringComparison.Ordinal)) > 0;
|
||||
if (removed)
|
||||
{
|
||||
_displayNameCache.Remove(hashedCid);
|
||||
}
|
||||
}
|
||||
|
||||
if (removed)
|
||||
@@ -134,23 +124,6 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (TryGetCachedDisplayName(hashedCid, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var resolved = ResolveDisplayNameInternal(hashedCid);
|
||||
if (!string.IsNullOrWhiteSpace(resolved))
|
||||
{
|
||||
CacheDisplayName(hashedCid, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private string ResolveDisplayNameInternal(string hashedCid)
|
||||
{
|
||||
var (name, address) = _dalamudUtil.FindPlayerByNameHash(hashedCid);
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
@@ -160,9 +133,8 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
||||
: name;
|
||||
}
|
||||
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
var pair = snapshot.PairsByUid.Values
|
||||
.Where(p => !string.IsNullOrEmpty(p.GetPlayerNameHash()))
|
||||
var pair = _pairManager
|
||||
.GetOnlineUserPairs()
|
||||
.FirstOrDefault(p => string.Equals(p.Ident, hashedCid, StringComparison.Ordinal));
|
||||
|
||||
if (pair != null)
|
||||
@@ -208,90 +180,10 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var removedAny = false;
|
||||
for (var i = _requests.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var entry = _requests[i];
|
||||
if (now - entry.ReceivedAt <= _expiration)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_displayNameCache.Remove(entry.HashedCid);
|
||||
_requests.RemoveAt(i);
|
||||
removedAny = true;
|
||||
}
|
||||
|
||||
return removedAny;
|
||||
}
|
||||
|
||||
public void AcceptPairRequest(string hashedCid, string displayName)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _apiController.Value.TryPairWithContentId(hashedCid).ConfigureAwait(false);
|
||||
RemoveRequest(hashedCid);
|
||||
|
||||
var displayText = string.IsNullOrEmpty(displayName) ? hashedCid : displayName;
|
||||
Mediator.Publish(new NotificationMessage(
|
||||
"Pair request accepted",
|
||||
$"Sent a pair request back to {displayText}.",
|
||||
NotificationType.Info,
|
||||
TimeSpan.FromSeconds(3)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to accept pair request for {HashedCid}", hashedCid);
|
||||
Mediator.Publish(new NotificationMessage(
|
||||
"Failed to Accept Pair Request",
|
||||
ex.Message,
|
||||
NotificationType.Error,
|
||||
TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void DeclinePairRequest(string hashedCid, string displayName)
|
||||
{
|
||||
RemoveRequest(hashedCid);
|
||||
Mediator.Publish(new NotificationMessage("Pair request declined",
|
||||
"Declined " + displayName + "'s pending pair request.",
|
||||
NotificationType.Info,
|
||||
TimeSpan.FromSeconds(3)));
|
||||
Logger.LogDebug("Declined pair request from {HashedCid}", hashedCid);
|
||||
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);
|
||||
|
||||
private bool TryGetCachedDisplayName(string hashedCid, out string displayName)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(hashedCid) && _displayNameCache.TryGetValue(hashedCid, out var cached))
|
||||
{
|
||||
displayName = cached;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
displayName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CacheDisplayName(string hashedCid, string displayName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hashedCid) || string.IsNullOrWhiteSpace(displayName) || string.Equals(hashedCid, displayName, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_displayNameCache[hashedCid] = displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ public sealed class PerformanceCollectorService : IHostedService
|
||||
{
|
||||
if (!_lightlessConfigService.Current.LogPerformance) return func.Invoke();
|
||||
|
||||
var owner = sender.GetType().Name;
|
||||
var counter = counterName.BuildMessage();
|
||||
var cn = string.Concat(owner, _counterSplit, counter);
|
||||
string cn = sender.GetType().Name + _counterSplit + counterName.BuildMessage();
|
||||
|
||||
if (!PerformanceCounters.TryGetValue(cn, out var list))
|
||||
{
|
||||
list = PerformanceCounters[cn] = new(maxEntries);
|
||||
}
|
||||
|
||||
var dt = DateTime.UtcNow.Ticks;
|
||||
try
|
||||
@@ -53,12 +53,12 @@ public sealed class PerformanceCollectorService : IHostedService
|
||||
{
|
||||
if (!_lightlessConfigService.Current.LogPerformance) { act.Invoke(); return; }
|
||||
|
||||
var owner = sender.GetType().Name;
|
||||
var counter = counterName.BuildMessage();
|
||||
var cn = string.Concat(owner, _counterSplit, counter);
|
||||
var cn = sender.GetType().Name + _counterSplit + counterName.BuildMessage();
|
||||
|
||||
if (!PerformanceCounters.TryGetValue(cn, out var list))
|
||||
{
|
||||
list = PerformanceCounters[cn] = new(maxEntries);
|
||||
}
|
||||
|
||||
var dt = DateTime.UtcNow.Ticks;
|
||||
try
|
||||
@@ -72,7 +72,7 @@ public sealed class PerformanceCollectorService : IHostedService
|
||||
if (TimeSpan.FromTicks(elapsed) > TimeSpan.FromMilliseconds(10))
|
||||
_logger.LogWarning(">10ms spike on {counterName}: {time}", cn, TimeSpan.FromTicks(elapsed));
|
||||
#endif
|
||||
list.Add((TimeOnly.FromDateTime(DateTime.Now), elapsed));
|
||||
list.Add(new(TimeOnly.FromDateTime(DateTime.Now), elapsed));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,11 +121,11 @@ public sealed class PerformanceCollectorService : IHostedService
|
||||
sb.Append('|');
|
||||
sb.Append("-Counter Name".PadRight(longestCounterName, '-'));
|
||||
sb.AppendLine();
|
||||
var orderedData = data.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var previousCaller = SplitCounterKey(orderedData[0].Key).Owner;
|
||||
var orderedData = data.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var previousCaller = orderedData[0].Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0];
|
||||
foreach (var entry in orderedData)
|
||||
{
|
||||
var newCaller = SplitCounterKey(entry.Key).Owner;
|
||||
var newCaller = entry.Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0];
|
||||
if (!string.Equals(previousCaller, newCaller, StringComparison.Ordinal))
|
||||
{
|
||||
DrawSeparator(sb, longestCounterName);
|
||||
@@ -135,13 +135,13 @@ public sealed class PerformanceCollectorService : IHostedService
|
||||
|
||||
if (pastEntries.Any())
|
||||
{
|
||||
sb.Append((" " + TimeSpan.FromTicks(pastEntries.LastOrDefault() == default ? 0 : pastEntries[^1].Item2).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
|
||||
sb.Append((" " + TimeSpan.FromTicks(pastEntries.LastOrDefault() == default ? 0 : pastEntries.Last().Item2).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
|
||||
sb.Append('|');
|
||||
sb.Append((" " + TimeSpan.FromTicks(pastEntries.Max(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
|
||||
sb.Append('|');
|
||||
sb.Append((" " + TimeSpan.FromTicks((long)pastEntries.Average(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15));
|
||||
sb.Append('|');
|
||||
sb.Append((" " + (pastEntries.LastOrDefault() == default ? "-" : pastEntries[^1].Item1.ToString("HH:mm:ss.ffff", CultureInfo.InvariantCulture))).PadRight(15, ' '));
|
||||
sb.Append((" " + (pastEntries.LastOrDefault() == default ? "-" : pastEntries.Last().Item1.ToString("HH:mm:ss.ffff", CultureInfo.InvariantCulture))).PadRight(15, ' '));
|
||||
sb.Append('|');
|
||||
sb.Append((" " + pastEntries.Count).PadRight(10));
|
||||
sb.Append('|');
|
||||
@@ -157,12 +157,6 @@ public sealed class PerformanceCollectorService : IHostedService
|
||||
_logger.LogInformation("{perf}", sb.ToString());
|
||||
}
|
||||
|
||||
private static (string Owner, string Counter) SplitCounterKey(string cn)
|
||||
{
|
||||
var parts = cn.Split(_counterSplit, 2, StringSplitOptions.None);
|
||||
return (parts[0], parts.Length > 1 ? parts[1] : string.Empty);
|
||||
}
|
||||
|
||||
private static void DrawSeparator(StringBuilder sb, int longestCounterName)
|
||||
{
|
||||
sb.Append("".PadRight(15, '-'));
|
||||
@@ -189,7 +183,7 @@ public sealed class PerformanceCollectorService : IHostedService
|
||||
{
|
||||
try
|
||||
{
|
||||
var last = entries.Value.ToList()[^1];
|
||||
var last = entries.Value.ToList().Last();
|
||||
if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !PerformanceCounters.TryRemove(entries.Key, out _))
|
||||
{
|
||||
_logger.LogDebug("Could not remove performance counter {counter}", entries.Key);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.TextureCompression;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.WebAPI.Files.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -18,22 +17,20 @@ public class PlayerPerformanceService
|
||||
private readonly ILogger<PlayerPerformanceService> _logger;
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private readonly TextureDownscaleService _textureDownscaleService;
|
||||
private readonly Dictionary<string, bool> _warnedForPlayers = new(StringComparer.Ordinal);
|
||||
|
||||
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, LightlessMediator mediator,
|
||||
PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager,
|
||||
XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService)
|
||||
XivDataAnalyzer xivDataAnalyzer)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_xivDataAnalyzer = xivDataAnalyzer;
|
||||
_textureDownscaleService = textureDownscaleService;
|
||||
}
|
||||
|
||||
public async Task<bool> CheckBothThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData)
|
||||
public async Task<bool> CheckBothThresholds(PairHandler pairHandler, CharacterData charaData)
|
||||
{
|
||||
var config = _playerPerformanceConfigService.Current;
|
||||
bool notPausedAfterVram = ComputeAndAutoPauseOnVRAMUsageThresholds(pairHandler, charaData, []);
|
||||
@@ -42,37 +39,37 @@ public class PlayerPerformanceService
|
||||
if (!notPausedAfterTris) return false;
|
||||
|
||||
if (config.UIDsToIgnore
|
||||
.Exists(uid => string.Equals(uid, pairHandler.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pairHandler.UserData.UID, StringComparison.Ordinal)))
|
||||
.Exists(uid => string.Equals(uid, pairHandler.Pair.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pairHandler.Pair.UserData.UID, StringComparison.Ordinal)))
|
||||
return true;
|
||||
|
||||
|
||||
var vramUsage = pairHandler.LastAppliedApproximateVRAMBytes;
|
||||
var triUsage = pairHandler.LastAppliedDataTris;
|
||||
var vramUsage = pairHandler.Pair.LastAppliedApproximateVRAMBytes;
|
||||
var triUsage = pairHandler.Pair.LastAppliedDataTris;
|
||||
|
||||
bool isPrefPerm = pairHandler.HasStickyPermissions;
|
||||
bool isPrefPerm = pairHandler.Pair.UserPair.OwnPermissions.HasFlag(API.Data.Enum.UserPermissions.Sticky);
|
||||
|
||||
bool exceedsTris = CheckForThreshold(config.WarnOnExceedingThresholds, config.TrisWarningThresholdThousands * 1000L,
|
||||
bool exceedsTris = CheckForThreshold(config.WarnOnExceedingThresholds, config.TrisWarningThresholdThousands * 1000,
|
||||
triUsage, config.WarnOnPreferredPermissionsExceedingThresholds, isPrefPerm);
|
||||
bool exceedsVram = CheckForThreshold(config.WarnOnExceedingThresholds, config.VRAMSizeWarningThresholdMiB * 1024L * 1024L,
|
||||
bool exceedsVram = CheckForThreshold(config.WarnOnExceedingThresholds, config.VRAMSizeWarningThresholdMiB * 1024 * 1024,
|
||||
vramUsage, config.WarnOnPreferredPermissionsExceedingThresholds, isPrefPerm);
|
||||
|
||||
if (_warnedForPlayers.TryGetValue(pairHandler.UserData.UID, out bool hadWarning) && hadWarning)
|
||||
if (_warnedForPlayers.TryGetValue(pairHandler.Pair.UserData.UID, out bool hadWarning) && hadWarning)
|
||||
{
|
||||
_warnedForPlayers[pairHandler.UserData.UID] = exceedsTris || exceedsVram;
|
||||
_warnedForPlayers[pairHandler.Pair.UserData.UID] = exceedsTris || exceedsVram;
|
||||
return true;
|
||||
}
|
||||
|
||||
_warnedForPlayers[pairHandler.UserData.UID] = exceedsTris || exceedsVram;
|
||||
_warnedForPlayers[pairHandler.Pair.UserData.UID] = exceedsTris || exceedsVram;
|
||||
|
||||
if (exceedsVram)
|
||||
{
|
||||
_mediator.Publish(new EventMessage(new Event(pairHandler.PlayerName, pairHandler.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
||||
_mediator.Publish(new EventMessage(new Event(pairHandler.Pair.PlayerName, pairHandler.Pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
||||
$"Exceeds VRAM threshold: ({UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeWarningThresholdMiB} MiB)")));
|
||||
}
|
||||
|
||||
if (exceedsTris)
|
||||
{
|
||||
_mediator.Publish(new EventMessage(new Event(pairHandler.PlayerName, pairHandler.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
||||
_mediator.Publish(new EventMessage(new Event(pairHandler.Pair.PlayerName, pairHandler.Pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
||||
$"Exceeds triangle threshold: ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)")));
|
||||
}
|
||||
|
||||
@@ -81,40 +78,38 @@ public class PlayerPerformanceService
|
||||
string warningText = string.Empty;
|
||||
if (exceedsTris && !exceedsVram)
|
||||
{
|
||||
warningText = $"Player {pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeds your configured triangle warning threshold\n" +
|
||||
$"{triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles";
|
||||
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured triangle warning threshold (" +
|
||||
$"{triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles).";
|
||||
}
|
||||
else if (!exceedsTris)
|
||||
{
|
||||
warningText = $"Player {pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeds your configured VRAM warning threshold\n" +
|
||||
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB";
|
||||
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured VRAM warning threshold (" +
|
||||
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB).";
|
||||
}
|
||||
else
|
||||
{
|
||||
warningText = $"Player {pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeds both VRAM warning threshold and triangle warning threshold\n" +
|
||||
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB and {triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles";
|
||||
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds both VRAM warning threshold (" +
|
||||
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB) and " +
|
||||
$"triangle warning threshold ({triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles).";
|
||||
}
|
||||
|
||||
_mediator.Publish(new PerformanceNotificationMessage(
|
||||
$"{pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeds performance threshold(s)",
|
||||
warningText,
|
||||
pairHandler.UserData,
|
||||
pairHandler.IsPaused,
|
||||
pairHandler.PlayerName));
|
||||
_mediator.Publish(new NotificationMessage($"{pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds performance threshold(s)",
|
||||
warningText, LightlessConfiguration.Models.NotificationType.Warning));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> CheckTriangleUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData)
|
||||
public async Task<bool> CheckTriangleUsageThresholds(PairHandler pairHandler, CharacterData charaData)
|
||||
{
|
||||
var config = _playerPerformanceConfigService.Current;
|
||||
var pair = pairHandler.Pair;
|
||||
|
||||
long triUsage = 0;
|
||||
|
||||
if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements))
|
||||
{
|
||||
pairHandler.LastAppliedDataTris = 0;
|
||||
pair.LastAppliedDataTris = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -128,35 +123,31 @@ public class PlayerPerformanceService
|
||||
triUsage += await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
pairHandler.LastAppliedDataTris = triUsage;
|
||||
pair.LastAppliedDataTris = triUsage;
|
||||
|
||||
_logger.LogDebug("Calculated VRAM usage for {p}", pairHandler);
|
||||
|
||||
// no warning of any kind on ignored pairs
|
||||
if (config.UIDsToIgnore
|
||||
.Exists(uid => string.Equals(uid, pairHandler.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pairHandler.UserData.UID, StringComparison.Ordinal)))
|
||||
.Exists(uid => string.Equals(uid, pair.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pair.UserData.UID, StringComparison.Ordinal)))
|
||||
return true;
|
||||
|
||||
bool isPrefPerm = pairHandler.HasStickyPermissions;
|
||||
bool isPrefPerm = pair.UserPair.OwnPermissions.HasFlag(API.Data.Enum.UserPermissions.Sticky);
|
||||
|
||||
// now check auto pause
|
||||
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.TrisAutoPauseThresholdThousands * 1000L,
|
||||
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.TrisAutoPauseThresholdThousands * 1000,
|
||||
triUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
|
||||
{
|
||||
var message = $"Player {pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeded your configured triangle auto pause threshold and has been automatically paused\n" +
|
||||
$"{triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles";
|
||||
|
||||
_mediator.Publish(new PerformanceNotificationMessage(
|
||||
$"{pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) automatically paused",
|
||||
message,
|
||||
pairHandler.UserData,
|
||||
true,
|
||||
pairHandler.PlayerName));
|
||||
_mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused",
|
||||
$"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured triangle auto pause threshold (" +
|
||||
$"{triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)" +
|
||||
$" and has been automatically paused.",
|
||||
LightlessConfiguration.Models.NotificationType.Warning));
|
||||
|
||||
_mediator.Publish(new EventMessage(new Event(pairHandler.PlayerName, pairHandler.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
||||
_mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
||||
$"Exceeds triangle threshold: automatically paused ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)")));
|
||||
|
||||
_mediator.Publish(new PauseMessage(pairHandler.UserData));
|
||||
_mediator.Publish(new PauseMessage(pair.UserData));
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -164,18 +155,16 @@ public class PlayerPerformanceService
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
|
||||
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(PairHandler pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
|
||||
{
|
||||
var config = _playerPerformanceConfigService.Current;
|
||||
bool skipDownscale = pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions;
|
||||
var pair = pairHandler.Pair;
|
||||
|
||||
long vramUsage = 0;
|
||||
long effectiveVramUsage = 0;
|
||||
|
||||
if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements))
|
||||
{
|
||||
pairHandler.LastAppliedApproximateVRAMBytes = 0;
|
||||
pairHandler.LastAppliedApproximateEffectiveVRAMBytes = 0;
|
||||
pair.LastAppliedApproximateVRAMBytes = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -187,13 +176,11 @@ public class PlayerPerformanceService
|
||||
foreach (var hash in moddedTextureHashes)
|
||||
{
|
||||
long fileSize = 0;
|
||||
long effectiveSize = 0;
|
||||
|
||||
var download = toDownloadFiles.Find(f => string.Equals(hash, f.Hash, StringComparison.OrdinalIgnoreCase));
|
||||
if (download != null)
|
||||
{
|
||||
fileSize = download.TotalRaw;
|
||||
effectiveSize = fileSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -207,63 +194,35 @@ public class PlayerPerformanceService
|
||||
}
|
||||
|
||||
fileSize = fileEntry.Size.Value;
|
||||
effectiveSize = fileSize;
|
||||
|
||||
if (!skipDownscale)
|
||||
{
|
||||
var preferredPath = _textureDownscaleService.GetPreferredPath(hash, fileEntry.ResolvedFilepath);
|
||||
if (!string.IsNullOrEmpty(preferredPath) && File.Exists(preferredPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
effectiveSize = new FileInfo(preferredPath).Length;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogTrace(ex, "Failed to read size for preferred texture path {Path}", preferredPath);
|
||||
effectiveSize = fileSize;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
effectiveSize = fileSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vramUsage += fileSize;
|
||||
effectiveVramUsage += effectiveSize;
|
||||
}
|
||||
|
||||
pairHandler.LastAppliedApproximateVRAMBytes = vramUsage;
|
||||
pairHandler.LastAppliedApproximateEffectiveVRAMBytes = effectiveVramUsage;
|
||||
pair.LastAppliedApproximateVRAMBytes = vramUsage;
|
||||
|
||||
_logger.LogDebug("Calculated VRAM usage for {p}", pairHandler);
|
||||
|
||||
// no warning of any kind on ignored pairs
|
||||
if (config.UIDsToIgnore
|
||||
.Exists(uid => string.Equals(uid, pairHandler.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pairHandler.UserData.UID, StringComparison.Ordinal)))
|
||||
.Exists(uid => string.Equals(uid, pair.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pair.UserData.UID, StringComparison.Ordinal)))
|
||||
return true;
|
||||
|
||||
bool isPrefPerm = pairHandler.HasStickyPermissions;
|
||||
bool isPrefPerm = pair.UserPair.OwnPermissions.HasFlag(API.Data.Enum.UserPermissions.Sticky);
|
||||
|
||||
// now check auto pause
|
||||
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.VRAMSizeAutoPauseThresholdMiB * 1024L * 1024L,
|
||||
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.VRAMSizeAutoPauseThresholdMiB * 1024 * 1024,
|
||||
vramUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
|
||||
{
|
||||
var message = $"Player {pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeded your configured VRAM auto pause threshold and has been automatically paused\n" +
|
||||
$"{UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB}MiB";
|
||||
_mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused",
|
||||
$"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured VRAM auto pause threshold (" +
|
||||
$"{UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB}MiB)" +
|
||||
$" and has been automatically paused.",
|
||||
LightlessConfiguration.Models.NotificationType.Warning));
|
||||
|
||||
_mediator.Publish(new PerformanceNotificationMessage(
|
||||
$"{pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) automatically paused",
|
||||
message,
|
||||
pairHandler.UserData,
|
||||
true,
|
||||
pairHandler.PlayerName));
|
||||
_mediator.Publish(new PauseMessage(pair.UserData));
|
||||
|
||||
_mediator.Publish(new PauseMessage(pairHandler.UserData));
|
||||
|
||||
_mediator.Publish(new EventMessage(new Event(pairHandler.PlayerName, pairHandler.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
||||
_mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
||||
$"Exceeds VRAM threshold: automatically paused ({UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB} MiB)")));
|
||||
|
||||
return false;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace LightlessSync.Services.Profiles;
|
||||
|
||||
public record LightlessGroupProfileData(
|
||||
bool IsDisabled,
|
||||
bool IsNsfw,
|
||||
string Base64ProfilePicture,
|
||||
string Base64BannerPicture,
|
||||
string Description,
|
||||
IReadOnlyList<int> Tags)
|
||||
{
|
||||
public Lazy<byte[]> ProfileImageData { get; } = new(() => ConvertSafe(Base64ProfilePicture));
|
||||
public Lazy<byte[]> BannerImageData { get; } = new(() => ConvertSafe(Base64BannerPicture));
|
||||
|
||||
private static byte[] ConvertSafe(string value) => string.IsNullOrEmpty(value)
|
||||
? Array.Empty<byte>()
|
||||
: Convert.FromBase64String(value);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user