diff --git a/.gitea/workflows/lightless-tag-and-release.yml b/.gitea/workflows/lightless-tag-and-release.yml index d5b7266..a91d953 100644 --- a/.gitea/workflows/lightless-tag-and-release.yml +++ b/.gitea/workflows/lightless-tag-and-release.yml @@ -6,7 +6,9 @@ on: env: PLUGIN_NAME: LightlessSync - DOTNET_VERSION: 9.x + DOTNET_VERSION: | + 10.x.x + 9.x.x jobs: tag-and-release: @@ -16,15 +18,17 @@ jobs: steps: - name: Checkout Lightless - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - submodules: true + submodules: recursive - - name: Setup .NET 9 SDK - uses: actions/setup-dotnet@v4 + - name: Setup .NET 10 SDK + uses: actions/setup-dotnet@v5 with: - dotnet-version: 9.x + dotnet-version: | + 10.x.x + 9.x.x - name: Download Dalamud run: | diff --git a/.gitmodules b/.gitmodules index fe64c7f..7ae9eb6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,18 @@ [submodule "LightlessAPI"] path = LightlessAPI url = https://git.lightless-sync.org/Lightless-Sync/LightlessAPI.git -[submodule "PenumbraAPI"] - path = PenumbraAPI - url = https://github.com/Ottermandias/Penumbra.Api.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 diff --git a/LightlessAPI b/LightlessAPI index 0170ac3..8e4432a 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 0170ac377d7d2341c0d0e206ab871af22ac4767b +Subproject commit 8e4432af45c1955436afe309c93e019577ad10e5 diff --git a/LightlessSync.sln b/LightlessSync.sln index 5b7ca3c..55bddfd 100644 --- a/LightlessSync.sln +++ b/LightlessSync.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32328.378 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11217.181 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{585B740D-BA2C-429B-9CF3-B2D223423748}" ProjectSection(SolutionItems) = preProject @@ -12,40 +12,110 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessSync", "LightlessS EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessSync.API", "LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj", "{A4E42AFA-5045-7E81-937F-3A320AC52987}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.Api", "PenumbraAPI\Penumbra.Api.csproj", "{C104F6BE-9CC4-9CF7-271C-5C3A1F646601}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.Api", "Penumbra.Api\Penumbra.Api.csproj", "{22AE06C8-5139-45D2-A5F9-E76C019050D9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{3C016B19-2A2C-4068-9378-B9B805605EFB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OtterGui", "OtterGui\OtterGui.csproj", "{C77A2833-3FE4-405B-811D-439B1FF859D9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pictomancy", "ffxiv_pictomancy\Pictomancy\Pictomancy.csproj", "{825F17D8-2704-24F6-DF8B-2542AC92C765}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.ActiveCfg = Release|x64 - {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.Build.0 = Release|x64 + {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.ActiveCfg = Debug|x64 + {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|Any CPU.Build.0 = Debug|x64 {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x64.ActiveCfg = Debug|x64 {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x64.Build.0 = Debug|x64 + {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Debug|x86.Build.0 = Debug|Any CPU {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|Any CPU.ActiveCfg = Release|x64 {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|Any CPU.Build.0 = Release|x64 {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x64.ActiveCfg = Release|x64 {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x64.Build.0 = Release|x64 - {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.Build.0 = Release|Any CPU + {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x86.ActiveCfg = Release|Any CPU + {BB929046-4CD2-B174-EBAA-C756AC3AC8DA}.Release|x86.Build.0 = Release|Any CPU + {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x64.ActiveCfg = Debug|Any CPU {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x64.Build.0 = Debug|Any CPU + {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x86.ActiveCfg = Debug|Any CPU + {A4E42AFA-5045-7E81-937F-3A320AC52987}.Debug|x86.Build.0 = Debug|Any CPU {A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|Any CPU.Build.0 = Release|Any CPU {A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x64.ActiveCfg = Release|Any CPU {A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x64.Build.0 = Release|Any CPU - {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|Any CPU.ActiveCfg = Debug|x64 - {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|Any CPU.Build.0 = Debug|x64 - {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|x64.ActiveCfg = Debug|x64 - {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Debug|x64.Build.0 = Debug|x64 - {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|Any CPU.ActiveCfg = Release|x64 - {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|Any CPU.Build.0 = Release|x64 - {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|x64.ActiveCfg = Release|x64 - {C104F6BE-9CC4-9CF7-271C-5C3A1F646601}.Release|x64.Build.0 = Release|x64 + {A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x86.ActiveCfg = Release|Any CPU + {A4E42AFA-5045-7E81-937F-3A320AC52987}.Release|x86.Build.0 = Release|Any CPU + {82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|Any CPU.ActiveCfg = Debug|x64 + {82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|Any CPU.Build.0 = Debug|x64 + {82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|x64.ActiveCfg = Debug|x64 + {82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|x64.Build.0 = Debug|x64 + {82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|x86.ActiveCfg = Debug|x64 + {82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Debug|x86.Build.0 = Debug|x64 + {82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|Any CPU.ActiveCfg = Release|x64 + {82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|Any CPU.Build.0 = Release|x64 + {82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|x64.ActiveCfg = Release|x64 + {82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|x64.Build.0 = Release|x64 + {82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|x86.ActiveCfg = Release|x64 + {82DFB180-DD4C-4C48-919C-B5C5C77FC0FD}.Release|x86.Build.0 = Release|x64 + {22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|Any CPU.ActiveCfg = Debug|x64 + {22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|Any CPU.Build.0 = Debug|x64 + {22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|x64.ActiveCfg = Debug|x64 + {22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|x64.Build.0 = Debug|x64 + {22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|x86.ActiveCfg = Debug|x64 + {22AE06C8-5139-45D2-A5F9-E76C019050D9}.Debug|x86.Build.0 = Debug|x64 + {22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|Any CPU.ActiveCfg = Release|x64 + {22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|Any CPU.Build.0 = Release|x64 + {22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|x64.ActiveCfg = Release|x64 + {22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|x64.Build.0 = Release|x64 + {22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|x86.ActiveCfg = Release|x64 + {22AE06C8-5139-45D2-A5F9-E76C019050D9}.Release|x86.Build.0 = Release|x64 + {3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|Any CPU.ActiveCfg = Debug|x64 + {3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|Any CPU.Build.0 = Debug|x64 + {3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|x64.ActiveCfg = Debug|x64 + {3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|x64.Build.0 = Debug|x64 + {3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|x86.ActiveCfg = Debug|x64 + {3C016B19-2A2C-4068-9378-B9B805605EFB}.Debug|x86.Build.0 = Debug|x64 + {3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|Any CPU.ActiveCfg = Release|x64 + {3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|Any CPU.Build.0 = Release|x64 + {3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|x64.ActiveCfg = Release|x64 + {3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|x64.Build.0 = Release|x64 + {3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|x86.ActiveCfg = Release|x64 + {3C016B19-2A2C-4068-9378-B9B805605EFB}.Release|x86.Build.0 = Release|x64 + {C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|Any CPU.ActiveCfg = Debug|x64 + {C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|Any CPU.Build.0 = Debug|x64 + {C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|x64.ActiveCfg = Debug|x64 + {C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|x64.Build.0 = Debug|x64 + {C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|x86.ActiveCfg = Debug|x64 + {C77A2833-3FE4-405B-811D-439B1FF859D9}.Debug|x86.Build.0 = Debug|x64 + {C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|Any CPU.ActiveCfg = Release|x64 + {C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|Any CPU.Build.0 = Release|x64 + {C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x64.ActiveCfg = Release|x64 + {C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x64.Build.0 = Release|x64 + {C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x86.ActiveCfg = Release|x64 + {C77A2833-3FE4-405B-811D-439B1FF859D9}.Release|x86.Build.0 = Release|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|Any CPU.ActiveCfg = Debug|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|Any CPU.Build.0 = Debug|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x64.ActiveCfg = Debug|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x64.Build.0 = Debug|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x86.ActiveCfg = Debug|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Debug|x86.Build.0 = Debug|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|Any CPU.ActiveCfg = Release|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|Any CPU.Build.0 = Release|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.ActiveCfg = Release|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.Build.0 = Release|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.ActiveCfg = Release|x64 + {825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LightlessSync/Changelog/changelog.yaml b/LightlessSync/Changelog/changelog.yaml index 43b0c79..18a0a8c 100644 --- a/LightlessSync/Changelog/changelog.yaml +++ b/LightlessSync/Changelog/changelog.yaml @@ -1,11 +1,80 @@ -tagline: "Lightless Sync v1.12.4" -subline: "Bugfixes and various improvements across Lightless" +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" - # be sure to set this every new version - isCurrent: true versions: - number: "Syncshells" icon: "" diff --git a/LightlessSync/FileCache/CacheMonitor.cs b/LightlessSync/FileCache/CacheMonitor.cs index 486e11e..fde9b6d 100644 --- a/LightlessSync/FileCache/CacheMonitor.cs +++ b/LightlessSync/FileCache/CacheMonitor.cs @@ -6,6 +6,7 @@ using LightlessSync.Utils; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Collections.Immutable; +using System.IO; namespace LightlessSync.FileCache; @@ -21,6 +22,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase private CancellationTokenSource _scanCancellationTokenSource = new(); private readonly CancellationTokenSource _periodicCalculationTokenSource = new(); public static readonly IImmutableList AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"]; + private static readonly HashSet AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase); public CacheMonitor(ILogger logger, IpcManager ipcManager, LightlessConfigService configService, FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil, @@ -72,7 +74,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase { while (_dalamudUtil.IsOnFrameworkThread && !token.IsCancellationRequested) { - await Task.Delay(1).ConfigureAwait(false); + await Task.Delay(1, token).ConfigureAwait(false); } RecalculateFileCacheSize(token); @@ -101,8 +103,8 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase } record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null); - private readonly Dictionary _watcherChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _lightlessChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _watcherChanges = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _lightlessChanges = new(StringComparer.OrdinalIgnoreCase); public void StopMonitoring() { @@ -128,7 +130,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase } var fsType = FileSystemHelper.GetFilesystemType(_configService.Current.CacheFolder, _dalamudUtil.IsWine); - if (fsType == FileSystemHelper.FilesystemType.NTFS) + if (fsType == FileSystemHelper.FilesystemType.NTFS && !_dalamudUtil.IsWine) { StorageisNTFS = true; Logger.LogInformation("Lightless Storage is on NTFS drive: {isNtfs}", StorageisNTFS); @@ -163,7 +165,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase { Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath); - if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; + if (!HasAllowedExtension(e.FullPath)) return; lock (_watcherChanges) { @@ -207,7 +209,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase private void Fs_Changed(object sender, FileSystemEventArgs e) { if (Directory.Exists(e.FullPath)) return; - if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; + if (!HasAllowedExtension(e.FullPath)) return; if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created)) return; @@ -231,7 +233,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase { foreach (var file in directoryFiles) { - if (!AllowedFileExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) continue; + if (!HasAllowedExtension(file)) continue; var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase); _watcherChanges.Remove(oldPath); @@ -243,7 +245,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase } else { - if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; + if (!HasAllowedExtension(e.FullPath)) return; lock (_watcherChanges) { @@ -259,9 +261,21 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase private CancellationTokenSource _penumbraFswCts = new(); private CancellationTokenSource _lightlessFswCts = new(); + public FileSystemWatcher? PenumbraWatcher { get; private set; } public FileSystemWatcher? LightlessWatcher { get; private set; } + private static bool HasAllowedExtension(string path) + { + if (string.IsNullOrEmpty(path)) + { + return false; + } + + var extension = Path.GetExtension(path); + return !string.IsNullOrEmpty(extension) && AllowedFileExtensionSet.Contains(extension); + } + private async Task LightlessWatcherExecution() { _lightlessFswCts = _lightlessFswCts.CancelRecreate(); @@ -469,6 +483,52 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase FileCacheSize = totalSize; + if (Directory.Exists(_configService.Current.CacheFolder + "/downscaled")) + { + var filesDownscaled = Directory.EnumerateFiles(_configService.Current.CacheFolder + "/downscaled").Select(f => new FileInfo(f)).OrderBy(f => f.LastAccessTime).ToList(); + + long totalSizeDownscaled = 0; + + foreach (var f in filesDownscaled) + { + token.ThrowIfCancellationRequested(); + + try + { + long size = 0; + + if (!isWine) + { + try + { + size = _fileCompactor.GetFileSizeOnDisk(f); + } + catch (Exception ex) + { + Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName); + size = f.Length; + } + } + else + { + size = f.Length; + } + + totalSizeDownscaled += size; + } + catch (Exception ex) + { + Logger.LogTrace(ex, "Error getting size for {file}", f.FullName); + } + } + + FileCacheSize = (totalSize + totalSizeDownscaled); + } + else + { + FileCacheSize = totalSize; + } + var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d); if (FileCacheSize < maxCacheInBytes) return; @@ -510,12 +570,19 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase protected override void Dispose(bool disposing) { base.Dispose(disposing); - _scanCancellationTokenSource?.Cancel(); + // Disposing of file system watchers PenumbraWatcher?.Dispose(); LightlessWatcher?.Dispose(); + + // Disposing of cancellation token sources + _scanCancellationTokenSource?.CancelDispose(); + _scanCancellationTokenSource?.Dispose(); _penumbraFswCts?.CancelDispose(); + _penumbraFswCts?.Dispose(); _lightlessFswCts?.CancelDispose(); + _lightlessFswCts?.Dispose(); _periodicCalculationTokenSource?.CancelDispose(); + _periodicCalculationTokenSource?.Dispose(); } private void FullFileScan(CancellationToken ct) @@ -552,7 +619,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase [ .. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories) .AsParallel() - .Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase)) + .Where(f => HasAllowedExtension(f) && !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase) && !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase) && !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)), @@ -593,7 +660,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase List entitiesToRemove = []; List entitiesToUpdate = []; - object sync = new(); + Lock sync = new(); Thread[] workerThreads = new Thread[threadCount]; ConcurrentQueue fileCaches = new(_fileDbManager.GetAllFileCaches()); diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index 7ee6c99..e2cdc72 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -18,6 +18,7 @@ public sealed class FileCacheManager : IHostedService 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; @@ -41,11 +42,8 @@ public sealed class FileCacheManager : IHostedService private string CsvBakPath => _csvPath + ".bak"; - private static string NormalizeSeparators(string path) - { - return path.Replace("/", "\\", StringComparison.Ordinal) + private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal) .Replace("\\\\", "\\", StringComparison.Ordinal); - } private static string NormalizePrefixedPathKey(string prefixedPath) { @@ -134,13 +132,9 @@ public sealed class FileCacheManager : IHostedService chosenLength = penumbraMatch; } - if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch)) + if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch) && cacheMatch > chosenLength) { - if (cacheMatch > chosenLength) - { - chosenPrefixed = cachePrefixed; - chosenLength = cacheMatch; - } + chosenPrefixed = cachePrefixed; } return NormalizePrefixedPathKey(chosenPrefixed ?? normalized); @@ -176,27 +170,53 @@ public sealed class FileCacheManager : IHostedService return CreateFileCacheEntity(fi, prefixedPath); } - public List GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null)).ToList(); + public List GetAllFileCaches() => [.. _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null))]; public List GetAllFileCachesByHash(string hash, bool ignoreCacheEntries = false, bool validate = true) { - List output = []; - if (_fileCaches.TryGetValue(hash, out var fileCacheEntities)) + var output = new List(); + + if (!_fileCaches.TryGetValue(hash, out var fileCacheEntities)) + return output; + + foreach (var fileCache in fileCacheEntities.Values + .Where(c => !ignoreCacheEntries || !c.IsCacheEntry)) { - foreach (var fileCache in fileCacheEntities.Values.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList()) + if (!validate) { - if (!validate) - { - output.Add(fileCache); - } - else - { - var validated = GetValidatedFileCache(fileCache); - if (validated != null) - { - output.Add(validated); - } - } + output.Add(fileCache); + continue; + } + + var validated = GetValidatedFileCache(fileCache); + if (validated != null) + output.Add(validated); + } + + return output; + } + + public async Task> GetAllFileCachesByHashAsync(string hash, bool ignoreCacheEntries = false, bool validate = true,CancellationToken token = default) + { + var output = new List(); + + 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); } } @@ -238,10 +258,11 @@ public sealed class FileCacheManager : IHostedService return; } + var algo = Crypto.DetectAlgo(fileCache.Hash); string computedHash; try { - computedHash = await Crypto.GetFileHashAsync(fileCache.ResolvedFilepath, token).ConfigureAwait(false); + computedHash = await Crypto.ComputeFileHashAsync(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1, token).ConfigureAwait(false); } catch (Exception ex) { @@ -253,8 +274,8 @@ public sealed class FileCacheManager : IHostedService if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal)) { _logger.LogInformation( - "Hash mismatch: {file} (got {computedHash}, expected {expected})", - fileCache.ResolvedFilepath, computedHash, fileCache.Hash); + "Hash mismatch: {file} (got {computedHash}, expected {expected} : hash {hash})", + fileCache.ResolvedFilepath, computedHash, fileCache.Hash, algo); brokenEntities.Add(fileCache); } @@ -434,7 +455,7 @@ public sealed class FileCacheManager : IHostedService var fi = new FileInfo(fileCache.ResolvedFilepath); fileCache.Size = fi.Length; fileCache.CompressedSize = null; - fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath); + fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1); fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); } RemoveHashedFile(oldHash, prefixedPath); @@ -485,6 +506,44 @@ 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)) @@ -577,7 +636,7 @@ public sealed class FileCacheManager : IHostedService private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null) { - hash ??= Crypto.GetFileHash(fileInfo.FullName); + hash ??= Crypto.ComputeFileHash(fileInfo.FullName, Crypto.HashAlgo.Sha1); var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileInfo.Length); entity = ReplacePathPrefixes(entity); AddHashedFile(entity); @@ -585,13 +644,13 @@ public sealed class FileCacheManager : IHostedService { if (!File.Exists(_csvPath)) { - File.WriteAllLines(_csvPath, new[] { BuildVersionHeader(), entity.CsvEntry }); + File.WriteAllLines(_csvPath, [BuildVersionHeader(), entity.CsvEntry]); _csvHeaderEnsured = true; } else { EnsureCsvHeaderLockedCached(); - File.AppendAllLines(_csvPath, new[] { entity.CsvEntry }); + File.AppendAllLines(_csvPath, [entity.CsvEntry]); } } var result = GetFileCacheByPath(fileInfo.FullName); @@ -602,11 +661,17 @@ public sealed class FileCacheManager : IHostedService private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache) { var resultingFileCache = ReplacePathPrefixes(fileCache); - //_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath); resultingFileCache = Validate(resultingFileCache); return resultingFileCache; } + private async Task 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)) @@ -629,6 +694,7 @@ public sealed class FileCacheManager : IHostedService RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); return null; } + var file = new FileInfo(fileCache.ResolvedFilepath); if (!file.Exists) { @@ -636,7 +702,8 @@ public sealed class FileCacheManager : IHostedService return null; } - if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) + var lastWriteTicks = file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); + if (!string.Equals(lastWriteTicks, fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) { UpdateHashedFile(fileCache); } @@ -644,7 +711,34 @@ public sealed class FileCacheManager : IHostedService return fileCache; } - public Task StartAsync(CancellationToken cancellationToken) + private async Task ValidateAsync(FileCacheEntity fileCache, CancellationToken token) + { + if (string.IsNullOrWhiteSpace(fileCache.ResolvedFilepath)) + { + _logger.LogWarning("FileCacheEntity has empty ResolvedFilepath for hash {hash}, prefixed path {prefixed}", fileCache.Hash, fileCache.PrefixedFilePath); + RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); + return null; + } + + return await Task.Run(() => + { + var file = new FileInfo(fileCache.ResolvedFilepath); + if (!file.Exists) + { + RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); + return null; + } + + if (!string.Equals(file.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) + { + UpdateHashedFile(fileCache); + } + + return fileCache; + }, token).ConfigureAwait(false); + } + + public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting FileCacheManager"); @@ -695,14 +789,14 @@ public sealed class FileCacheManager : IHostedService try { _logger.LogInformation("Attempting to read {csvPath}", _csvPath); - entries = File.ReadAllLines(_csvPath); + entries = await File.ReadAllLinesAsync(_csvPath, cancellationToken).ConfigureAwait(false); success = true; } catch (Exception ex) { attempts++; _logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath); - Task.Delay(100, cancellationToken); + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } } @@ -729,7 +823,7 @@ public sealed class FileCacheManager : IHostedService BackupUnsupportedCache("invalid-version"); parseEntries = false; rewriteRequired = true; - entries = Array.Empty(); + entries = []; } else if (parsedVersion != FileCacheVersion) { @@ -737,7 +831,7 @@ public sealed class FileCacheManager : IHostedService BackupUnsupportedCache($"v{parsedVersion}"); parseEntries = false; rewriteRequired = true; - entries = Array.Empty(); + entries = []; } else { @@ -817,18 +911,18 @@ public sealed class FileCacheManager : IHostedService if (rewriteRequired) { - WriteOutFullCsv(); + await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false); } } _logger.LogInformation("Started FileCacheManager"); - - return Task.CompletedTask; + _lightlessMediator.Publish(new FileCacheInitializedMessage()); + await Task.CompletedTask.ConfigureAwait(false); } - public Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken) { - WriteOutFullCsv(); - return Task.CompletedTask; + await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false); + await Task.CompletedTask.ConfigureAwait(false); } } \ No newline at end of file diff --git a/LightlessSync/FileCache/FileCompactor.cs b/LightlessSync/FileCache/FileCompactor.cs index 3edf96a..771f558 100644 --- a/LightlessSync/FileCache/FileCompactor.cs +++ b/LightlessSync/FileCache/FileCompactor.cs @@ -4,6 +4,7 @@ using LightlessSync.Services.Compactor; using Microsoft.Extensions.Logging; using Microsoft.Win32.SafeHandles; using System.Collections.Concurrent; +using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading.Channels; @@ -11,7 +12,7 @@ using static LightlessSync.Utils.FileSystemHelper; namespace LightlessSync.FileCache; -public sealed class FileCompactor : IDisposable +public sealed partial class FileCompactor : IDisposable { public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U; public const ulong WOF_PROVIDER_FILE = 2UL; @@ -29,23 +30,26 @@ public sealed class FileCompactor : IDisposable private readonly SemaphoreSlim _globalGate; //Limit btrfs gate on half of threads given to compactor. - private static readonly SemaphoreSlim _btrfsGate = new(4, 4); + private readonly SemaphoreSlim _btrfsGate; private readonly BatchFilefragService _fragBatch; - private readonly WOF_FILE_COMPRESSION_INFO_V1 _efInfo = new() + private readonly bool _isWindows; + private readonly int _workerCount; + + private readonly WofFileCompressionInfoV1 _efInfo = new() { Algorithm = (int)CompressionAlgorithm.XPRESS8K, Flags = 0 }; [StructLayout(LayoutKind.Sequential, Pack = 1)] - private struct WOF_FILE_COMPRESSION_INFO_V1 + private struct WofFileCompressionInfoV1 { public int Algorithm; public ulong Flags; } - private enum CompressionAlgorithm + private enum CompressionAlgorithm { NO_COMPRESSION = -2, LZNT1 = -1, @@ -61,6 +65,7 @@ public sealed class FileCompactor : IDisposable _logger = logger; _lightlessConfigService = lightlessConfigService; _dalamudUtilService = dalamudUtilService; + _isWindows = OperatingSystem.IsWindows(); _compactionQueue = Channel.CreateUnbounded(new UnboundedChannelOptions { @@ -68,29 +73,36 @@ public sealed class FileCompactor : IDisposable SingleWriter = false }); + //Amount of threads given for the compactor int workers = Math.Clamp(Math.Min(Environment.ProcessorCount / 2, 4), 1, 8); + //Setup gates for the threads and setup worker count _globalGate = new SemaphoreSlim(workers, workers); - int workerCount = Math.Max(workers * 2, workers); + _btrfsGate = new SemaphoreSlim(workers / 2, workers / 2); + _workerCount = Math.Max(workers * 2, workers); - for (int i = 0; i < workerCount; i++) + //Setup workers on the queue + for (int i = 0; i < _workerCount; i++) { + int workerId = i; + _workers.Add(Task.Factory.StartNew( - () => ProcessQueueWorkerAsync(_compactionCts.Token), + () => ProcessQueueWorkerAsync(workerId, _compactionCts.Token), _compactionCts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap()); } + //Uses an batching service for the filefrag command on Linux _fragBatch = new BatchFilefragService( useShell: _dalamudUtilService.IsWine, log: _logger, batchSize: 64, - flushMs: 25, + flushMs: 25, runDirect: RunProcessDirect, runShell: RunProcessShell ); - _logger.LogInformation("FileCompactor started with {workers} workers", workerCount); + _logger.LogInformation("FileCompactor started with {workers} workers", _workerCount); } public bool MassCompactRunning { get; private set; } @@ -100,37 +112,91 @@ public sealed class FileCompactor : IDisposable /// Compact the storage of the Cache Folder /// /// Used to check if files needs to be compressed - public void CompactStorage(bool compress) + public void CompactStorage(bool compress, int? maxDegree = null) { MassCompactRunning = true; + try { - var allFiles = Directory.EnumerateFiles(_lightlessConfigService.Current.CacheFolder).ToList(); - int total = allFiles.Count; - int current = 0; - - foreach (var file in allFiles) + var folder = _lightlessConfigService.Current.CacheFolder; + if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder)) { - current++; - Progress = $"{current}/{total}"; + if (_logger.IsEnabled(LogLevel.Warning)) + _logger.LogWarning("Filecompacator couldnt find your Cache folder: {folder}", folder); + Progress = "0/0"; + return; + } + + var files = Directory.EnumerateFiles(folder).ToArray(); + var total = files.Length; + Progress = $"0/{total}"; + if (total == 0) return; + + var degree = maxDegree ?? Math.Clamp(Environment.ProcessorCount / 2, 1, 8); + + var done = 0; + int workerCounter = -1; + var po = new ParallelOptions + { + MaxDegreeOfParallelism = degree, + CancellationToken = _compactionCts.Token + }; + + Parallel.ForEach(files, po, localInit: () => Interlocked.Increment(ref workerCounter), body: (file, state, workerId) => + { + _globalGate.WaitAsync(po.CancellationToken).GetAwaiter().GetResult(); + + if (!_pendingCompactions.TryAdd(file, 0)) + return -1; try { - // Compress or decompress files - if (compress) - CompactFile(file); - else - DecompressFile(file); + try + { + if (compress) + { + if (_lightlessConfigService.Current.UseCompactor) + CompactFile(file, workerId); + } + else + { + DecompressFile(file, workerId); + } + } + catch (IOException ioEx) + { + _logger.LogDebug(ioEx, "[W{worker}] File being read/written, skipping file: {file}", workerId, file); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "[W{worker}] Error processing file: {file}", workerId, file); + } + finally + { + var n = Interlocked.Increment(ref done); + Progress = $"{n}/{total}"; + } } - catch (IOException ioEx) + finally { - _logger.LogDebug(ioEx, "File {file} locked or busy, skipping", file); + _pendingCompactions.TryRemove(file, out _); + _globalGate.Release(); } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error compacting/decompressing file {file}", file); - } - } + + return workerId; + }, + localFinally: _ => + { + //Ignore local finally for now + }); + } + catch (OperationCanceledException ex) + { + _logger.LogDebug(ex, "Mass compaction call got cancelled, shutting off compactor."); } finally { @@ -139,6 +205,7 @@ public sealed class FileCompactor : IDisposable } } + /// /// Write all bytes into a directory async /// @@ -197,24 +264,20 @@ public sealed class FileCompactor : IDisposable { try { - bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var (_, linuxPath) = ResolvePathsForBtrfs(fileInfo.FullName); var (ok, output, err, code) = - isWindowsProc + _isWindows ? RunProcessShell($"stat -c='%b' {QuoteSingle(linuxPath)}", workingDir: null, 10000) : RunProcessDirect("stat", ["-c='%b'", linuxPath], workingDir: null, 10000); - if (ok && long.TryParse(output.Trim(), out long blocks)) - return (false, blocks * 512L); // st_blocks are always 512B units - - _logger.LogDebug("Btrfs size probe failed for {linux} (stat {code}, err {err}). Falling back to Length.", linuxPath, code, err); - return (false, fileInfo.Length); + return (flowControl: false, value: fileInfo.Length); } catch (Exception ex) { - _logger.LogDebug(ex, "Failed Btrfs size probe for {file}, using Length", fileInfo.FullName); - return (false, fileInfo.Length); + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug(ex, "Failed Btrfs size probe for {file}, using Length", fileInfo.FullName); + return (flowControl: true, value: fileInfo.Length); } } @@ -228,34 +291,48 @@ public sealed class FileCompactor : IDisposable try { var blockSize = GetBlockSizeForPath(fileInfo.FullName, _logger, _dalamudUtilService.IsWine); - var losize = GetCompressedFileSizeW(fileInfo.FullName, out uint hosize); - var size = (long)hosize << 32 | losize; - return (flowControl: false, value: ((size + blockSize - 1) / blockSize) * blockSize); + if (blockSize <= 0) + throw new InvalidOperationException($"Invalid block size {blockSize} for {fileInfo.FullName}"); + + uint lo = GetCompressedFileSizeW(fileInfo.FullName, out uint hi); + + if (lo == 0xFFFFFFFF) + { + int err = Marshal.GetLastWin32Error(); + if (err != 0) + throw new Win32Exception(err); + } + + long size = ((long)hi << 32) | lo; + long rounded = ((size + blockSize - 1) / blockSize) * blockSize; + + return (flowControl: false, value: rounded); } catch (Exception ex) { _logger.LogDebug(ex, "Failed stat size for {file}, fallback to Length", fileInfo.FullName); + return (flowControl: true, value: default); } - - return (flowControl: true, value: default); } /// /// Compressing the given path with BTRFS or NTFS file system. /// - /// Path of the decompressed/normal file - private void CompactFile(string filePath) + /// Path of the decompressed/normal file + /// Worker/Process Id + private void CompactFile(string filePath, int workerId) { var fi = new FileInfo(filePath); if (!fi.Exists) { - _logger.LogTrace("Skip compaction: missing {file}", filePath); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("[W{worker}] Skip compaction: missing {file}", workerId, filePath); return; } var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine); var oldSize = fi.Length; - int blockSize = GetBlockSizeForPath(fi.FullName, _logger, _dalamudUtilService.IsWine); + int blockSize = (int)(GetFileSizeOnDisk(fi) / 512); // We skipping small files (128KiB) as they slow down the system a lot for BTRFS. as BTRFS has a different blocksize it requires an different calculation. long minSizeBytes = fsType == FilesystemType.Btrfs @@ -264,7 +341,8 @@ public sealed class FileCompactor : IDisposable if (oldSize < minSizeBytes) { - _logger.LogTrace("Skip compaction: {file} ({size} B) < threshold ({th} B)", filePath, oldSize, minSizeBytes); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("[W{worker}] Skip compaction: {file} ({size} B) < threshold ({th} B)", workerId, filePath, oldSize, minSizeBytes); return; } @@ -272,20 +350,20 @@ public sealed class FileCompactor : IDisposable { if (!IsWOFCompactedFile(filePath)) { - _logger.LogDebug("NTFS compaction XPRESS8K: {file}", filePath); if (WOFCompressFile(filePath)) { var newSize = GetFileSizeOnDisk(fi); - _logger.LogDebug("NTFS compressed {file} {old} -> {new}", filePath, oldSize, newSize); + _logger.LogDebug("[W{worker}] NTFS compressed XPRESS8K {file} {old} -> {new}", workerId, filePath, oldSize, newSize); } else { - _logger.LogWarning("NTFS compression failed or unavailable for {file}", filePath); + _logger.LogWarning("[W{worker}] NTFS compression failed or unavailable for {file}", workerId, filePath); } } else { - _logger.LogTrace("Already NTFS-compressed: {file}", filePath); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("[W{worker}] Already NTFS-compressed with XPRESS8K: {file}", workerId, filePath); } return; } @@ -294,41 +372,43 @@ public sealed class FileCompactor : IDisposable { if (!IsBtrfsCompressedFile(filePath)) { - _logger.LogDebug("Btrfs compression zstd: {file}", filePath); if (BtrfsCompressFile(filePath)) { var newSize = GetFileSizeOnDisk(fi); - _logger.LogDebug("Btrfs compressed {file} {old} -> {new}", filePath, oldSize, newSize); + _logger.LogDebug("[W{worker}] Btrfs compressed clzo {file} {old} -> {new}", workerId, filePath, oldSize, newSize); } else { - _logger.LogWarning("Btrfs compression failed or unavailable for {file}", filePath); + _logger.LogWarning("[W{worker}] Btrfs compression failed or unavailable for {file}", workerId, filePath); } } else { - _logger.LogTrace("Already Btrfs-compressed: {file}", filePath); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("[W{worker}] Already Btrfs-compressed with clzo: {file}", workerId, filePath); } return; } - _logger.LogTrace("Skip compact: unsupported FS for {file}", filePath); + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("[W{worker}] Skip compact: unsupported FS for {file}", workerId, filePath); } /// /// Decompressing the given path with BTRFS file system or NTFS file system. /// - /// Path of the compressed file - private void DecompressFile(string path) + /// Path of the decompressed/normal file + /// Worker/Process Id + private void DecompressFile(string filePath, int workerId) { - _logger.LogDebug("Decompress request: {file}", path); - var fsType = GetFilesystemType(path, _dalamudUtilService.IsWine); + _logger.LogDebug("[W{worker}] Decompress request: {file}", workerId, filePath); + var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine); if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine) { try { - bool flowControl = DecompressWOFFile(path); + bool flowControl = DecompressWOFFile(filePath, workerId); if (!flowControl) { return; @@ -336,7 +416,7 @@ public sealed class FileCompactor : IDisposable } catch (Exception ex) { - _logger.LogWarning(ex, "NTFS decompress error {file}", path); + _logger.LogWarning(ex, "[W{worker}] NTFS decompress error {file}", workerId, filePath); } } @@ -344,7 +424,7 @@ public sealed class FileCompactor : IDisposable { try { - bool flowControl = DecompressBtrfsFile(path); + bool flowControl = DecompressBtrfsFile(filePath); if (!flowControl) { return; @@ -352,7 +432,7 @@ public sealed class FileCompactor : IDisposable } catch (Exception ex) { - _logger.LogWarning(ex, "Btrfs decompress error {file}", path); + _logger.LogWarning(ex, "[W{worker}] Btrfs decompress error {file}", workerId, filePath); } } } @@ -372,51 +452,48 @@ public sealed class FileCompactor : IDisposable string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path; var opts = GetMountOptionsForPath(linuxPath); - bool hasCompress = opts.Contains("compress", StringComparison.OrdinalIgnoreCase); - bool hasCompressForce = opts.Contains("compress-force", StringComparison.OrdinalIgnoreCase); + if (!string.IsNullOrEmpty(opts)) + _logger.LogTrace("Mount opts for {file}: {opts}", linuxPath, opts); - if (hasCompressForce) + var probe = RunProcessShell("command -v btrfs || which btrfs", timeoutMs: 5000); + var _btrfsAvailable = probe.ok && !string.IsNullOrWhiteSpace(probe.stdout); + if (!_btrfsAvailable) + _logger.LogWarning("btrfs cli not found in path. Compression will be skipped."); + + var prop = isWine + ? RunProcessShell($"btrfs property set -- {QuoteSingle(linuxPath)} compression none", timeoutMs: 15000) + : RunProcessDirect("btrfs", ["property", "set", "--", linuxPath, "compression", "none"], "/", 15000); + + if (prop.ok) _logger.LogTrace("Set per-file 'compression none' on {file}", linuxPath); + else _logger.LogTrace("btrfs property set failed for {file} (exit {code}): {err}", linuxPath, prop.exitCode, prop.stderr); + + var defrag = isWine + ? RunProcessShell($"btrfs filesystem defragment -f -- {QuoteSingle(linuxPath)}", timeoutMs: 60000) + : RunProcessDirect("btrfs", ["filesystem", "defragment", "-f", "--", linuxPath], "/", 60000); + + if (!defrag.ok) { - _logger.LogWarning("Cannot safely decompress {file}: mount options contains compress-force ({opts}).", linuxPath, opts); + _logger.LogWarning("btrfs defragment (decompress) failed for {file} (exit {code}): {err}", + linuxPath, defrag.exitCode, defrag.stderr); return false; } - if (hasCompress) - { - var setCmd = $"btrfs property set -- {QuoteDouble(linuxPath)} compression none"; - var (okSet, _, errSet, codeSet) = isWine - ? RunProcessShell(setCmd) - : RunProcessDirect("btrfs", ["property", "set", "--", linuxPath, "compression", "none"]); - - if (!okSet) - { - _logger.LogWarning("Failed to set 'compression none' on {file}, please check drive options (exit code is: {code}): {err}", linuxPath, codeSet, errSet); - return false; - } - _logger.LogTrace("Set per-file 'compression none' on {file}", linuxPath); - } - - if (!IsBtrfsCompressedFile(linuxPath)) - { - _logger.LogTrace("{file} is not compressed, skipping decompression completely", linuxPath); - return true; - } - - var (ok, stdout, stderr, code) = isWine - ? RunProcessShell($"btrfs filesystem defragment -- {QuoteDouble(linuxPath)}") - : RunProcessDirect("btrfs", ["filesystem", "defragment", "--", linuxPath]); - - if (!ok) - { - _logger.LogWarning("btrfs defragment (decompress) failed for {file} (exit code is: {code}): {stderr}", - linuxPath, code, stderr); - return false; - } - - if (!string.IsNullOrWhiteSpace(stdout)) - _logger.LogTrace("btrfs defragment output for {file}: {out}", linuxPath, stdout.Trim()); + if (!string.IsNullOrWhiteSpace(defrag.stdout)) + _logger.LogTrace("btrfs defragment output for {file}: {out}", linuxPath, defrag.stdout.Trim()); _logger.LogInformation("Decompressed (rewritten uncompressed) Btrfs file: {file}", linuxPath); + + try + { + if (_fragBatch != null) + { + var compressed = _fragBatch.IsCompressedAsync(linuxPath, _compactionCts.Token).GetAwaiter().GetResult(); + if (compressed) + _logger.LogTrace("Post-check: {file} still shows 'compressed' flag (may be stale).", linuxPath); + } + } + catch { /* ignore verification noisy */ } + return true; } catch (Exception ex) @@ -432,18 +509,18 @@ public sealed class FileCompactor : IDisposable /// /// Path of the compressed file /// Decompressing state - private bool DecompressWOFFile(string path) + private bool DecompressWOFFile(string path, int workerID) { //Check if its already been compressed if (TryIsWofExternal(path, out bool isExternal, out int algo)) { if (!isExternal) { - _logger.LogTrace("Already decompressed file: {file}", path); + _logger.LogTrace("[W{worker}] Already decompressed file: {file}", workerID, path); return true; } var compressString = ((CompressionAlgorithm)algo).ToString(); - _logger.LogTrace("WOF compression (algo={algo}) detected for {file}", compressString, path); + _logger.LogTrace("[W{worker}] WOF compression (algo={algo}) detected for {file}", workerID, compressString, path); } //This will attempt to start WOF thread. @@ -457,15 +534,15 @@ public sealed class FileCompactor : IDisposable // 342 error code means its been decompressed after the control, we handle it as it succesfully been decompressed. if (err == 342) { - _logger.LogTrace("Successfully decompressed NTFS file {file}", path); + _logger.LogTrace("[W{worker}] Successfully decompressed NTFS file {file}", workerID, path); return true; } - _logger.LogWarning("DeviceIoControl failed for {file} with Win32 error {err}", path, err); + _logger.LogWarning("[W{worker}] DeviceIoControl failed for {file} with Win32 error {err}", workerID, path, err); return false; } - _logger.LogTrace("Successfully decompressed NTFS file {file}", path); + _logger.LogTrace("[W{worker}] Successfully decompressed NTFS file {file}", workerID, path); return true; }); } @@ -478,6 +555,7 @@ public sealed class FileCompactor : IDisposable /// Converted path to be used in Linux private string ToLinuxPathIfWine(string path, bool isWine, bool preferShell = true) { + //Return if not wine if (!isWine || !IsProbablyWine()) return path; @@ -539,7 +617,7 @@ public sealed class FileCompactor : IDisposable /// Compessing state private bool WOFCompressFile(string path) { - int size = Marshal.SizeOf(); + int size = Marshal.SizeOf(); IntPtr efInfoPtr = Marshal.AllocHGlobal(size); try @@ -592,7 +670,7 @@ public sealed class FileCompactor : IDisposable { try { - uint buf = (uint)Marshal.SizeOf(); + uint buf = (uint)Marshal.SizeOf(); int result = WofIsExternalFile(filePath, out int isExternal, out _, out var info, ref buf); if (result != 0 || isExternal == 0) return false; @@ -621,7 +699,7 @@ public sealed class FileCompactor : IDisposable algorithm = 0; try { - uint buf = (uint)Marshal.SizeOf(); + uint buf = (uint)Marshal.SizeOf(); int hr = WofIsExternalFile(path, out int ext, out _, out var info, ref buf); if (hr == 0 && ext != 0) { @@ -630,13 +708,13 @@ public sealed class FileCompactor : IDisposable } return true; } - catch (DllNotFoundException) + catch (DllNotFoundException) { - return false; + return false; } - catch (EntryPointNotFoundException) - { - return false; + catch (EntryPointNotFoundException) + { + return false; } } @@ -651,8 +729,7 @@ public sealed class FileCompactor : IDisposable { try { - bool windowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - string linuxPath = windowsProc ? ResolveLinuxPathForWine(path) : path; + string linuxPath = _isWindows ? ResolveLinuxPathForWine(path) : path; var task = _fragBatch.IsCompressedAsync(linuxPath, _compactionCts.Token); @@ -685,7 +762,6 @@ public sealed class FileCompactor : IDisposable try { var (winPath, linuxPath) = ResolvePathsForBtrfs(path); - bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); if (IsBtrfsCompressedFile(linuxPath)) { @@ -699,8 +775,13 @@ public sealed class FileCompactor : IDisposable return false; } + var probe = RunProcessShell("command -v btrfs || which btrfs", timeoutMs: 5000); + var _btrfsAvailable = probe.ok && !string.IsNullOrWhiteSpace(probe.stdout); + if (!_btrfsAvailable) + _logger.LogWarning("btrfs cli not found in path. Compression will be skipped."); + (bool ok, string stdout, string stderr, int code) = - isWindowsProc + _isWindows ? RunProcessShell($"btrfs filesystem defragment -clzo -- {QuoteSingle(linuxPath)}") : RunProcessDirect("btrfs", ["filesystem", "defragment", "-clzo", "--", linuxPath]); @@ -783,9 +864,10 @@ public sealed class FileCompactor : IDisposable RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, - CreateNoWindow = true + CreateNoWindow = true, + WorkingDirectory = workingDir ?? "/", }; - if (!string.IsNullOrEmpty(workingDir)) psi.WorkingDirectory = workingDir; + foreach (var a in args) psi.ArgumentList.Add(a); EnsureUnixPathEnv(psi); @@ -799,8 +881,18 @@ public sealed class FileCompactor : IDisposable } int code; - try { code = proc.ExitCode; } catch { code = -1; } - return (code == 0, so2, se2, code); + try { code = proc.ExitCode; } + catch { code = -1; } + + bool ok = code == 0; + + if (!ok && code == -1 && + string.IsNullOrWhiteSpace(se2) && !string.IsNullOrWhiteSpace(so2)) + { + ok = true; + } + + return (ok, so2, se2, code); } /// @@ -811,15 +903,14 @@ public sealed class FileCompactor : IDisposable /// State of the process, output of the process and error with exit code private (bool ok, string stdout, string stderr, int exitCode) RunProcessShell(string command, string? workingDir = null, int timeoutMs = 60000) { - var psi = new ProcessStartInfo("/bin/bash") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, - CreateNoWindow = true + CreateNoWindow = true, + WorkingDirectory = workingDir ?? "/", }; - if (!string.IsNullOrEmpty(workingDir)) psi.WorkingDirectory = workingDir; // Use a Login shell so PATH includes /usr/sbin etc. AKA -lc for login shell psi.ArgumentList.Add("-lc"); @@ -836,65 +927,72 @@ public sealed class FileCompactor : IDisposable } int code; - try { code = proc.ExitCode; } catch { code = -1; } - return (code == 0, so2, se2, code); + try { code = proc.ExitCode; } + catch { code = -1; } + + bool ok = code == 0; + + if (!ok && code == -1 && string.IsNullOrWhiteSpace(se2) && !string.IsNullOrWhiteSpace(so2)) + { + ok = true; + } + + return (ok, so2, se2, code); } /// /// Checking the process result for shell or direct processes /// /// Process - /// How long when timeout is gotten + /// How long when timeout goes over threshold /// Cancellation Token /// Multiple variables - private (bool success, string testy, string testi) CheckProcessResult(Process proc, int timeoutMs, CancellationToken token) + private (bool success, string output, string errorCode) CheckProcessResult(Process proc, int timeoutMs, CancellationToken token) { var outTask = proc.StandardOutput.ReadToEndAsync(token); var errTask = proc.StandardError.ReadToEndAsync(token); var bothTasks = Task.WhenAll(outTask, errTask); - //On wine, we dont wanna use waitforexit as it will be always broken and giving an error. - if (_dalamudUtilService.IsWine) - { - var finished = Task.WhenAny(bothTasks, Task.Delay(timeoutMs, token)).GetAwaiter().GetResult(); - if (finished != bothTasks) - { - try - { - proc.Kill(entireProcessTree: true); - Task.WaitAll([outTask, errTask], 1000, token); - } - catch - { - // ignore this - } - var so = outTask.IsCompleted ? outTask.Result : ""; - var se = errTask.IsCompleted ? errTask.Result : "timeout"; - return (false, so, se); - } + var finished = Task.WhenAny(bothTasks, Task.Delay(timeoutMs, token)).GetAwaiter().GetResult(); - var stderr = errTask.Result; - var ok = string.IsNullOrWhiteSpace(stderr); - return (ok, outTask.Result, stderr); + if (token.IsCancellationRequested) + return KillProcess(proc, outTask, errTask, token); + + if (finished != bothTasks) + return KillProcess(proc, outTask, errTask, token); + + bool isWine = _dalamudUtilService?.IsWine ?? false; + if (!isWine) + { + try { proc.WaitForExit(); } catch { /* ignore quirks */ } + } + else + { + var sw = Stopwatch.StartNew(); + while (!proc.HasExited && sw.ElapsedMilliseconds < 75) + Thread.Sleep(5); } - // On linux, we can use it as we please - if (!proc.WaitForExit(timeoutMs)) - { - try - { - proc.Kill(entireProcessTree: true); - Task.WaitAll([outTask, errTask], 1000, token); - } - catch - { - // ignore this - } - return (false, outTask.IsCompleted ? outTask.Result : "", "timeout"); - } + var stdout = outTask.Status == TaskStatus.RanToCompletion ? outTask.Result : ""; + var stderr = errTask.Status == TaskStatus.RanToCompletion ? errTask.Result : ""; - Task.WaitAll(outTask, errTask); - return (true, outTask.Result, errTask.Result); + int code = -1; + try { if (proc.HasExited) code = proc.ExitCode; } catch { /* Wine may still throw */ } + + bool ok = code == 0 || (isWine && string.IsNullOrWhiteSpace(stderr)); + + return (ok, stdout, stderr); + + static (bool success, string output, string errorCode) KillProcess( + Process proc, Task outTask, Task errTask, CancellationToken token) + { + try { proc.Kill(entireProcessTree: true); } catch { /* ignore */ } + try { Task.WaitAll([outTask, errTask], 1000, token); } catch { /* ignore */ } + + var so = outTask.IsCompleted ? outTask.Result : ""; + var se = errTask.IsCompleted ? errTask.Result : "canceled/timeout"; + return (false, so, se); + } } /// @@ -954,10 +1052,10 @@ public sealed class FileCompactor : IDisposable } /// - /// Process the queue with, meant for a worker/thread + /// Process the queue, meant for a worker/thread /// /// Cancellation token for the worker whenever it needs to be stopped - private async Task ProcessQueueWorkerAsync(CancellationToken token) + private async Task ProcessQueueWorkerAsync(int workerId, CancellationToken token) { try { @@ -973,7 +1071,7 @@ public sealed class FileCompactor : IDisposable try { if (_lightlessConfigService.Current.UseCompactor && File.Exists(filePath)) - CompactFile(filePath); + CompactFile(filePath, workerId); } finally { @@ -992,8 +1090,8 @@ public sealed class FileCompactor : IDisposable } } } - catch (OperationCanceledException) - { + catch (OperationCanceledException) + { // Shutting down worker, this exception is expected } } @@ -1005,7 +1103,7 @@ public sealed class FileCompactor : IDisposable /// Linux path to be used in Linux private string ResolveLinuxPathForWine(string windowsPath) { - var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(windowsPath)}", null, 5000); + var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(windowsPath)}", workingDir: null, 5000); if (ok && !string.IsNullOrWhiteSpace(outp)) return outp.Trim(); return ToLinuxPathIfWine(windowsPath, isWine: true); } @@ -1029,9 +1127,7 @@ public sealed class FileCompactor : IDisposable /// private (string windowsPath, string linuxPath) ResolvePathsForBtrfs(string path) { - bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - - if (!isWindowsProc) + if (!_isWindows) return (path, path); var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(path)}", workingDir: null, 5000); @@ -1050,7 +1146,7 @@ public sealed class FileCompactor : IDisposable { try { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (_isWindows) { using var _ = new FileStream(winePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); } @@ -1060,7 +1156,11 @@ public sealed class FileCompactor : IDisposable } return true; } - catch { return false; } + catch (Exception ex) + { + _logger.LogTrace(ex, "Probe open failed for {file} (linux={linux})", winePath, linuxPath); + return false; + } } /// @@ -1085,17 +1185,18 @@ public sealed class FileCompactor : IDisposable } - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, out uint lpBytesReturned, IntPtr lpOverlapped); + [LibraryImport("kernel32.dll", SetLastError = true)] + private static partial uint GetCompressedFileSizeW([MarshalAs(UnmanagedType.LPWStr)] string lpFileName, out uint lpFileSizeHigh); - [DllImport("kernel32.dll")] - private static extern uint GetCompressedFileSizeW([In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName, [Out, MarshalAs(UnmanagedType.U4)] out uint lpFileSizeHigh); + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, out uint lpBytesReturned, IntPtr lpOverlapped); - [DllImport("WofUtil.dll")] - private static extern int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WOF_FILE_COMPRESSION_INFO_V1 Info, ref uint BufferLength); + [LibraryImport("WofUtil.dll")] + private static partial int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WofFileCompressionInfoV1 Info, ref uint BufferLength); - [DllImport("WofUtil.dll", SetLastError = true)] - private static extern int WofSetFileDataLocation(SafeFileHandle FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length); + [LibraryImport("WofUtil.dll")] + private static partial int WofSetFileDataLocation(SafeFileHandle FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length); private static string QuoteSingle(string s) => "'" + s.Replace("'", "'\\''", StringComparison.Ordinal) + "'"; @@ -1103,7 +1204,11 @@ public sealed class FileCompactor : IDisposable public void Dispose() { + //Cleanup of gates and frag service _fragBatch?.Dispose(); + _btrfsGate?.Dispose(); + _globalGate?.Dispose(); + _compactionQueue.Writer.TryComplete(); _compactionCts.Cancel(); @@ -1111,8 +1216,8 @@ public sealed class FileCompactor : IDisposable { Task.WaitAll([.. _workers.Where(t => t != null)], TimeSpan.FromSeconds(5)); } - catch - { + catch + { // Ignore this catch on the dispose } finally diff --git a/LightlessSync/FileCache/FileState.cs b/LightlessSync/FileCache/FileState.cs index dfad917..0a1088b 100644 --- a/LightlessSync/FileCache/FileState.cs +++ b/LightlessSync/FileCache/FileState.cs @@ -5,4 +5,5 @@ public enum FileState Valid, RequireUpdate, RequireDeletion, + RequireRehash } \ No newline at end of file diff --git a/LightlessSync/FileCache/TransientResourceManager.cs b/LightlessSync/FileCache/TransientResourceManager.cs index 6a9575a..2ef8f22 100644 --- a/LightlessSync/FileCache/TransientResourceManager.cs +++ b/LightlessSync/FileCache/TransientResourceManager.cs @@ -3,11 +3,17 @@ using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.PlayerData.Data; using LightlessSync.PlayerData.Handlers; +using LightlessSync.PlayerData.Factories; using LightlessSync.Services; +using LightlessSync.Services.ActorTracking; using LightlessSync.Services.Mediator; using LightlessSync.Utils; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; +using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace LightlessSync.FileCache; @@ -17,21 +23,29 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase private readonly HashSet _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 _playerRelatedPointers = []; - private ConcurrentDictionary _cachedFrameAddresses = []; + private readonly Dictionary _ownedHandlers = new(); + private ConcurrentDictionary _cachedFrameAddresses = new(); private ConcurrentDictionary>? _semiTransientResources = null; private uint _lastClassJobId = uint.MaxValue; public bool IsTransientRecording { get; private set; } = false; public TransientResourceManager(ILogger logger, TransientConfigService configurationService, - DalamudUtilService dalamudUtil, LightlessMediator mediator) : base(logger, mediator) + DalamudUtilService dalamudUtil, LightlessMediator mediator, ActorObjectService actorObjectService, GameObjectHandlerFactory gameObjectHandlerFactory) : base(logger, mediator) { _configurationService = configurationService; _dalamudUtil = dalamudUtil; + _actorObjectService = actorObjectService; + _gameObjectHandlerFactory = gameObjectHandlerFactory; Mediator.Subscribe(this, Manager_PenumbraResourceLoadEvent); + Mediator.Subscribe(this, msg => HandleActorTracked(msg.Descriptor)); + Mediator.Subscribe(this, msg => HandleActorUntracked(msg.Descriptor)); Mediator.Subscribe(this, (_) => Manager_PenumbraModSettingChanged()); Mediator.Subscribe(this, (_) => DalamudUtil_FrameworkUpdate()); Mediator.Subscribe(this, (msg) => @@ -44,6 +58,11 @@ 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 @@ -123,12 +142,21 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase return; } - var transientResources = resources.ToList(); - Logger.LogDebug("Persisting {count} transient resources", transientResources.Count); - List newlyAddedGamePaths = resources.Except(semiTransientResources, StringComparer.Ordinal).ToList(); - foreach (var gamePath in transientResources) + List transientResources; + lock (resources) { - semiTransientResources.Add(gamePath); + transientResources = resources.ToList(); + } + + Logger.LogDebug("Persisting {count} transient resources", transientResources.Count); + List newlyAddedGamePaths; + lock (semiTransientResources) + { + newlyAddedGamePaths = transientResources.Except(semiTransientResources, StringComparer.Ordinal).ToList(); + foreach (var gamePath in transientResources) + { + semiTransientResources.Add(gamePath); + } } bool saveConfig = false; @@ -161,7 +189,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase _configurationService.Save(); } - TransientResources[objectKind].Clear(); + lock (resources) + { + resources.Clear(); + } } public void RemoveTransientResource(ObjectKind objectKind, string path) @@ -241,16 +272,46 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase TransientResources.Clear(); SemiTransientResources.Clear(); + + lock (_ownedHandlerLock) + { + foreach (var handler in _ownedHandlers.Values) + { + handler.Dispose(); + } + _ownedHandlers.Clear(); + } } private void DalamudUtil_FrameworkUpdate() { - _cachedFrameAddresses = new(_playerRelatedPointers.Where(k => k.Address != nint.Zero).ToDictionary(c => c.Address, c => c.ObjectKind)); lock (_cacheAdditionLock) { _cachedHandledPaths.Clear(); } + var activeDescriptors = new Dictionary(); + 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; @@ -259,16 +320,15 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase value?.Clear(); } - // reload config for current new classjob PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData); SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase); PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData); SemiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []]; } - foreach (var kind in Enum.GetValues(typeof(ObjectKind))) + foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast()) { - if (!_cachedFrameAddresses.Any(k => k.Value == (ObjectKind)kind) && TransientResources.Remove((ObjectKind)kind, out _)) + if (!_cachedFrameAddresses.Any(k => k.Value == kind) && TransientResources.Remove(kind, out _)) { Logger.LogDebug("Object not present anymore: {kind}", kind.ToString()); } @@ -292,6 +352,119 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase _semiTransientResources = null; } + private static bool TryResolveObjectKind(ActorObjectService.ActorDescriptor descriptor, out ObjectKind resolvedKind) + { + if (descriptor.OwnedKind is ObjectKind ownedKind) + { + resolvedKind = ownedKind; + return true; + } + + if (descriptor.ObjectKind == DalamudObjectKind.Player) + { + resolvedKind = ObjectKind.Player; + return true; + } + + resolvedKind = default; + return false; + } + + private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor) + { + if (descriptor.IsInGpose) + return; + + if (!TryResolveObjectKind(descriptor, out var resolvedKind)) + return; + + if (Logger.IsEnabled(LogLevel.Debug)) + { + Logger.LogDebug("ActorObject tracked: {kind} addr={address:X} name={name}", resolvedKind, descriptor.Address, descriptor.Name); + } + + _cachedFrameAddresses[descriptor.Address] = resolvedKind; + + if (descriptor.OwnedKind is not ObjectKind ownedKind) + return; + + lock (_ownedHandlerLock) + { + if (_ownedHandlers.ContainsKey(descriptor.Address)) + return; + + _ = CreateOwnedHandlerAsync(descriptor, ownedKind); + } + } + + private void HandleActorUntracked(ActorObjectService.ActorDescriptor descriptor) + { + if (Logger.IsEnabled(LogLevel.Debug)) + { + var kindLabel = descriptor.OwnedKind?.ToString() + ?? (descriptor.ObjectKind == DalamudObjectKind.Player ? ObjectKind.Player.ToString() : ""); + Logger.LogDebug("ActorObject untracked: addr={address:X} name={name} kind={kind}", descriptor.Address, descriptor.Name, kindLabel); + } + + _cachedFrameAddresses.TryRemove(descriptor.Address, out _); + + if (descriptor.OwnedKind is not ObjectKind) + return; + + lock (_ownedHandlerLock) + { + if (_ownedHandlers.Remove(descriptor.Address, out var handler)) + { + handler.Dispose(); + } + } + } + + private async Task CreateOwnedHandlerAsync(ActorObjectService.ActorDescriptor descriptor, ObjectKind kind) + { + try + { + var handler = await _gameObjectHandlerFactory.Create( + kind, + () => + { + if (!string.IsNullOrEmpty(descriptor.HashedContentId) && + _actorObjectService.TryGetValidatedActorByHash(descriptor.HashedContentId, out var current) && + current.OwnedKind == kind) + { + return current.Address; + } + + return descriptor.Address; + }, + true).ConfigureAwait(false); + + if (handler.Address == IntPtr.Zero) + { + handler.Dispose(); + return; + } + + lock (_ownedHandlerLock) + { + if (!_cachedFrameAddresses.ContainsKey(descriptor.Address)) + { + Logger.LogDebug("ActorObject handler discarded (stale): addr={address:X}", descriptor.Address); + handler.Dispose(); + return; + } + + _ownedHandlers[descriptor.Address] = handler; + } + + Logger.LogDebug("ActorObject handler created: {kind} addr={address:X}", kind, descriptor.Address); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to create owned handler for {kind} at {address:X}", kind, descriptor.Address); + } + } + private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg) { var gamePath = msg.GamePath.ToLowerInvariant(); @@ -383,21 +556,30 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase private void SendTransients(nint gameObject, ObjectKind objectKind) { + _sendTransientCts.Cancel(); + _sendTransientCts = new(); + var token = _sendTransientCts.Token; + _ = Task.Run(async () => { - _sendTransientCts?.Cancel(); - _sendTransientCts?.Dispose(); - _sendTransientCts = new(); - var token = _sendTransientCts.Token; - await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false); - foreach (var kvp in TransientResources) + try { + await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false); + if (TransientResources.TryGetValue(objectKind, out var values) && values.Any()) { Logger.LogTrace("Sending Transients for {kind}", objectKind); Mediator.Publish(new TransientResourceChangedMessage(gameObject)); } } + catch (TaskCanceledException) + { + + } + catch (System.OperationCanceledException) + { + + } }); } diff --git a/LightlessSync/Interop/DalamudLogger.cs b/LightlessSync/Interop/DalamudLogger.cs index 3a833b9..24fcac2 100644 --- a/LightlessSync/Interop/DalamudLogger.cs +++ b/LightlessSync/Interop/DalamudLogger.cs @@ -20,7 +20,10 @@ internal sealed class DalamudLogger : ILogger _hasModifiedGameFiles = hasModifiedGameFiles; } - public IDisposable BeginScope(TState state) => default!; + IDisposable? ILogger.BeginScope(TState state) + { + return default!; + } public bool IsEnabled(LogLevel logLevel) { diff --git a/LightlessSync/Interop/Ipc/Framework/IpcFramework.cs b/LightlessSync/Interop/Ipc/Framework/IpcFramework.cs new file mode 100644 index 0000000..a68367a --- /dev/null +++ b/LightlessSync/Interop/Ipc/Framework/IpcFramework.cs @@ -0,0 +1,196 @@ +using Dalamud.Plugin; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using System.Linq; + +namespace LightlessSync.Interop.Ipc.Framework; + +public enum IpcConnectionState +{ + Unknown = 0, + MissingPlugin = 1, + VersionMismatch = 2, + PluginDisabled = 3, + NotReady = 4, + Available = 5, + Error = 6, +} + +public sealed record IpcServiceDescriptor(string InternalName, string DisplayName, Version MinimumVersion) +{ + public override string ToString() + => $"{DisplayName} (>= {MinimumVersion})"; +} + +public interface IIpcService : IDisposable +{ + IpcServiceDescriptor Descriptor { get; } + IpcConnectionState State { get; } + IDalamudPluginInterface PluginInterface { get; } + bool APIAvailable { get; } + void CheckAPI(); +} + +public interface IIpcInterop : IDisposable +{ + string Name { get; } + void OnConnectionStateChanged(IpcConnectionState state); +} + +public abstract class IpcInteropBase : IIpcInterop +{ + protected IpcInteropBase(ILogger logger) + { + Logger = logger; + } + + protected ILogger Logger { get; } + + protected IpcConnectionState State { get; private set; } = IpcConnectionState.Unknown; + + protected bool IsAvailable => State == IpcConnectionState.Available; + + public abstract string Name { get; } + + public void OnConnectionStateChanged(IpcConnectionState state) + { + if (State == state) + { + return; + } + + var previous = State; + State = state; + HandleStateChange(previous, state); + } + + protected abstract void HandleStateChange(IpcConnectionState previous, IpcConnectionState current); + + public virtual void Dispose() + { + } +} + +public abstract class IpcServiceBase : DisposableMediatorSubscriberBase, IIpcService +{ + private readonly List _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 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(); + } +} diff --git a/LightlessSync/Interop/Ipc/IIpcCaller.cs b/LightlessSync/Interop/Ipc/IIpcCaller.cs deleted file mode 100644 index 8519d1a..0000000 --- a/LightlessSync/Interop/Ipc/IIpcCaller.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace LightlessSync.Interop.Ipc; - -public interface IIpcCaller : IDisposable -{ - bool APIAvailable { get; } - void CheckAPI(); -} diff --git a/LightlessSync/Interop/Ipc/IpcCallerBrio.cs b/LightlessSync/Interop/Ipc/IpcCallerBrio.cs index 5728464..98836a4 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerBrio.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerBrio.cs @@ -1,69 +1,63 @@ -using Dalamud.Game.ClientState.Objects.Types; +using Brio.API; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; using LightlessSync.API.Dto.CharaData; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; +using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; using System.Numerics; using System.Text.Json.Nodes; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerBrio : IIpcCaller +public sealed class IpcCallerBrio : IpcServiceBase { + private static readonly IpcServiceDescriptor BrioDescriptor = new("Brio", "Brio", new Version(0, 0, 0, 0)); + private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtilService; - private readonly ICallGateSubscriber<(int, int)> _brioApiVersion; - private readonly ICallGateSubscriber> _brioSpawnActorAsync; - private readonly ICallGateSubscriber _brioDespawnActor; - private readonly ICallGateSubscriber _brioSetModelTransform; - private readonly ICallGateSubscriber _brioGetModelTransform; - private readonly ICallGateSubscriber _brioGetPoseAsJson; - private readonly ICallGateSubscriber _brioSetPoseFromJson; - private readonly ICallGateSubscriber _brioFreezeActor; - private readonly ICallGateSubscriber _brioFreezePhysics; + private readonly ApiVersion _apiVersion; + private readonly SpawnActor _spawnActor; + private readonly DespawnActor _despawnActor; + private readonly SetModelTransform _setModelTransform; + private readonly GetModelTransform _getModelTransform; - public bool APIAvailable { get; private set; } + private readonly GetPoseAsJson _getPoseAsJson; + private readonly LoadPoseFromJson _setPoseFromJson; + + private readonly FreezeActor _freezeActor; + private readonly FreezePhysics _freezePhysics; public IpcCallerBrio(ILogger logger, IDalamudPluginInterface dalamudPluginInterface, - DalamudUtilService dalamudUtilService) + DalamudUtilService dalamudUtilService, LightlessMediator mediator) : base(logger, mediator, dalamudPluginInterface, BrioDescriptor) { _logger = logger; _dalamudUtilService = dalamudUtilService; - _brioApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("Brio.ApiVersion"); - _brioSpawnActorAsync = dalamudPluginInterface.GetIpcSubscriber>("Brio.Actor.SpawnExAsync"); - _brioDespawnActor = dalamudPluginInterface.GetIpcSubscriber("Brio.Actor.Despawn"); - _brioSetModelTransform = dalamudPluginInterface.GetIpcSubscriber("Brio.Actor.SetModelTransform"); - _brioGetModelTransform = dalamudPluginInterface.GetIpcSubscriber("Brio.Actor.GetModelTransform"); - _brioGetPoseAsJson = dalamudPluginInterface.GetIpcSubscriber("Brio.Actor.Pose.GetPoseAsJson"); - _brioSetPoseFromJson = dalamudPluginInterface.GetIpcSubscriber("Brio.Actor.Pose.LoadFromJson"); - _brioFreezeActor = dalamudPluginInterface.GetIpcSubscriber("Brio.Actor.Freeze"); - _brioFreezePhysics = dalamudPluginInterface.GetIpcSubscriber("Brio.FreezePhysics"); + _apiVersion = new ApiVersion(dalamudPluginInterface); + _spawnActor = new SpawnActor(dalamudPluginInterface); + _despawnActor = new DespawnActor(dalamudPluginInterface); + + _setModelTransform = new SetModelTransform(dalamudPluginInterface); + _getModelTransform = new GetModelTransform(dalamudPluginInterface); + + _getPoseAsJson = new GetPoseAsJson(dalamudPluginInterface); + _setPoseFromJson = new LoadPoseFromJson(dalamudPluginInterface); + + _freezeActor = new FreezeActor(dalamudPluginInterface); + _freezePhysics = new FreezePhysics(dalamudPluginInterface); CheckAPI(); } - public void CheckAPI() - { - try - { - var version = _brioApiVersion.InvokeFunc(); - APIAvailable = (version.Item1 == 2 && version.Item2 >= 0); - } - catch - { - APIAvailable = false; - } - } - public async Task SpawnActorAsync() { if (!APIAvailable) return null; _logger.LogDebug("Spawning Brio Actor"); - return await _brioSpawnActorAsync.InvokeFunc(false, false, true).ConfigureAwait(false); + return await _dalamudUtilService.RunOnFrameworkThread(() => _spawnActor.Invoke(Brio.API.Enums.SpawnFlags.Default, true)).ConfigureAwait(false); } public async Task DespawnActorAsync(nint address) @@ -72,7 +66,7 @@ public sealed class IpcCallerBrio : IIpcCaller var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false); if (gameObject == null) return false; _logger.LogDebug("Despawning Brio Actor {actor}", gameObject.Name.TextValue); - return await _dalamudUtilService.RunOnFrameworkThread(() => _brioDespawnActor.InvokeFunc(gameObject)).ConfigureAwait(false); + return await _dalamudUtilService.RunOnFrameworkThread(() => _despawnActor.Invoke(gameObject)).ConfigureAwait(false); } public async Task ApplyTransformAsync(nint address, WorldData data) @@ -82,7 +76,7 @@ public sealed class IpcCallerBrio : IIpcCaller if (gameObject == null) return false; _logger.LogDebug("Applying Transform to Actor {actor}", gameObject.Name.TextValue); - return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetModelTransform.InvokeFunc(gameObject, + return await _dalamudUtilService.RunOnFrameworkThread(() => _setModelTransform.Invoke(gameObject, new Vector3(data.PositionX, data.PositionY, data.PositionZ), new Quaternion(data.RotationX, data.RotationY, data.RotationZ, data.RotationW), new Vector3(data.ScaleX, data.ScaleY, data.ScaleZ), false)).ConfigureAwait(false); @@ -93,8 +87,7 @@ public sealed class IpcCallerBrio : IIpcCaller if (!APIAvailable) return default; var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false); if (gameObject == null) return default; - var data = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetModelTransform.InvokeFunc(gameObject)).ConfigureAwait(false); - //_logger.LogDebug("Getting Transform from Actor {actor}", gameObject.Name.TextValue); + var data = await _dalamudUtilService.RunOnFrameworkThread(() => _getModelTransform.Invoke(gameObject)).ConfigureAwait(false); return new WorldData() { @@ -118,7 +111,7 @@ public sealed class IpcCallerBrio : IIpcCaller if (gameObject == null) return null; _logger.LogDebug("Getting Pose from Actor {actor}", gameObject.Name.TextValue); - return await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false); + return await _dalamudUtilService.RunOnFrameworkThread(() => _getPoseAsJson.Invoke(gameObject)).ConfigureAwait(false); } public async Task SetPoseAsync(nint address, string pose) @@ -129,18 +122,41 @@ public sealed class IpcCallerBrio : IIpcCaller _logger.LogDebug("Setting Pose to Actor {actor}", gameObject.Name.TextValue); var applicablePose = JsonNode.Parse(pose)!; - var currentPose = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false); + var currentPose = await _dalamudUtilService.RunOnFrameworkThread(() => _getPoseAsJson.Invoke(gameObject)).ConfigureAwait(false); applicablePose["ModelDifference"] = JsonNode.Parse(JsonNode.Parse(currentPose)!["ModelDifference"]!.ToJsonString()); await _dalamudUtilService.RunOnFrameworkThread(() => { - _brioFreezeActor.InvokeFunc(gameObject); - _brioFreezePhysics.InvokeFunc(); + _freezeActor.Invoke(gameObject); + _freezePhysics.Invoke(); }).ConfigureAwait(false); - return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetPoseFromJson.InvokeFunc(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false); + return await _dalamudUtilService.RunOnFrameworkThread(() => _setPoseFromJson.Invoke(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false); } - public void Dispose() + protected override IpcConnectionState EvaluateState() { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + var version = _apiVersion.Invoke(); + return version.Breaking == 3 && version.Feature >= 0 + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Brio IPC version"); + return IpcConnectionState.Error; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerCustomize.cs b/LightlessSync/Interop/Ipc/IpcCallerCustomize.cs index 60feaba..fd1b88c 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerCustomize.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerCustomize.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Dalamud.Utility; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; @@ -9,8 +10,10 @@ using System.Text; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerCustomize : IIpcCaller +public sealed class IpcCallerCustomize : IpcServiceBase { + private static readonly IpcServiceDescriptor CustomizeDescriptor = new("CustomizePlus", "Customize+", new Version(0, 0, 0, 0)); + private readonly ICallGateSubscriber<(int, int)> _customizePlusApiVersion; private readonly ICallGateSubscriber _customizePlusGetActiveProfile; private readonly ICallGateSubscriber _customizePlusGetProfileById; @@ -23,7 +26,7 @@ public sealed class IpcCallerCustomize : IIpcCaller private readonly LightlessMediator _lightlessMediator; public IpcCallerCustomize(ILogger logger, IDalamudPluginInterface dalamudPluginInterface, - DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator) + DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator) : base(logger, lightlessMediator, dalamudPluginInterface, CustomizeDescriptor) { _customizePlusApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("CustomizePlus.General.GetApiVersion"); _customizePlusGetActiveProfile = dalamudPluginInterface.GetIpcSubscriber("CustomizePlus.Profile.GetActiveProfileIdOnCharacter"); @@ -41,8 +44,6 @@ public sealed class IpcCallerCustomize : IIpcCaller CheckAPI(); } - public bool APIAvailable { get; private set; } = false; - public async Task RevertAsync(nint character) { if (!APIAvailable) return; @@ -113,16 +114,25 @@ public sealed class IpcCallerCustomize : IIpcCaller return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale)); } - public void CheckAPI() + protected override IpcConnectionState EvaluateState() { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + try { var version = _customizePlusApiVersion.InvokeFunc(); - APIAvailable = (version.Item1 == 6 && version.Item2 >= 0); + return version.Item1 == 6 && version.Item2 >= 0 + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; } - catch + catch (Exception ex) { - APIAvailable = false; + Logger.LogDebug(ex, "Failed to query Customize+ API version"); + return IpcConnectionState.Error; } } @@ -132,8 +142,14 @@ public sealed class IpcCallerCustomize : IIpcCaller _lightlessMediator.Publish(new CustomizePlusMessage(obj?.Address ?? null)); } - public void Dispose() + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + if (!disposing) + { + return; + } + _customizePlusOnScaleUpdate.Unsubscribe(OnCustomizePlusScaleChange); } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerGlamourer.cs b/LightlessSync/Interop/Ipc/IpcCallerGlamourer.cs index 8763188..4f9f53d 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerGlamourer.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerGlamourer.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin; using Glamourer.Api.Helpers; using Glamourer.Api.IpcSubscribers; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; @@ -10,8 +11,9 @@ using Microsoft.Extensions.Logging; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcCaller +public sealed class IpcCallerGlamourer : IpcServiceBase { + private static readonly IpcServiceDescriptor GlamourerDescriptor = new("Glamourer", "Glamourer", new Version(1, 3, 0, 10)); private readonly ILogger _logger; private readonly IDalamudPluginInterface _pi; private readonly DalamudUtilService _dalamudUtil; @@ -31,7 +33,7 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC private readonly uint LockCode = 0x6D617265; public IpcCallerGlamourer(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator, - RedrawManager redrawManager) : base(logger, lightlessMediator) + RedrawManager redrawManager) : base(logger, lightlessMediator, pi, GlamourerDescriptor) { _glamourerApiVersions = new ApiVersion(pi); _glamourerGetAllCustomization = new GetStateBase64(pi); @@ -62,47 +64,6 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC _glamourerStateChanged?.Dispose(); } - public bool APIAvailable { get; private set; } - - public void CheckAPI() - { - bool apiAvailable = false; - try - { - bool versionValid = (_pi.InstalledPlugins - .FirstOrDefault(p => string.Equals(p.InternalName, "Glamourer", StringComparison.OrdinalIgnoreCase)) - ?.Version ?? new Version(0, 0, 0, 0)) >= new Version(1, 3, 0, 10); - try - { - var version = _glamourerApiVersions.Invoke(); - if (version is { Major: 1, Minor: >= 1 } && versionValid) - { - apiAvailable = true; - } - } - catch - { - // ignore - } - _shownGlamourerUnavailable = _shownGlamourerUnavailable && !apiAvailable; - - APIAvailable = apiAvailable; - } - catch - { - APIAvailable = apiAvailable; - } - finally - { - if (!apiAvailable && !_shownGlamourerUnavailable) - { - _shownGlamourerUnavailable = true; - _lightlessMediator.Publish(new NotificationMessage("Glamourer inactive", "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Lightless. If you just updated Glamourer, ignore this message.", - NotificationType.Error)); - } - } - } - public async Task ApplyAllAsync(ILogger logger, GameObjectHandler handler, string? customization, Guid applicationId, CancellationToken token, bool fireAndForget = false) { if (!APIAvailable || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return; @@ -210,6 +171,49 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC } } + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + var version = _glamourerApiVersions.Invoke(); + return version is { Major: 1, Minor: >= 1 } + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Glamourer API version"); + return IpcConnectionState.Error; + } + } + + protected override void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current) + { + base.OnConnectionStateChanged(previous, current); + + if (current == IpcConnectionState.Available) + { + _shownGlamourerUnavailable = false; + return; + } + + if (_shownGlamourerUnavailable || current == IpcConnectionState.Unknown) + { + return; + } + + _shownGlamourerUnavailable = true; + _lightlessMediator.Publish(new NotificationMessage("Glamourer inactive", + "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Lightless. If you just updated Glamourer, ignore this message.", + NotificationType.Error)); + } + private void GlamourerChanged(nint address) { _lightlessMediator.Publish(new GlamourerChangedMessage(address)); diff --git a/LightlessSync/Interop/Ipc/IpcCallerHeels.cs b/LightlessSync/Interop/Ipc/IpcCallerHeels.cs index 69b359c..23fe192 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerHeels.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerHeels.cs @@ -1,13 +1,16 @@ using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerHeels : IIpcCaller +public sealed class IpcCallerHeels : IpcServiceBase { + private static readonly IpcServiceDescriptor HeelsDescriptor = new("SimpleHeels", "Simple Heels", new Version(0, 0, 0, 0)); + private readonly ILogger _logger; private readonly LightlessMediator _lightlessMediator; private readonly DalamudUtilService _dalamudUtil; @@ -18,6 +21,7 @@ public sealed class IpcCallerHeels : IIpcCaller private readonly ICallGateSubscriber _heelsUnregisterPlayer; public IpcCallerHeels(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator) + : base(logger, lightlessMediator, pi, HeelsDescriptor) { _logger = logger; _lightlessMediator = lightlessMediator; @@ -32,8 +36,26 @@ public sealed class IpcCallerHeels : IIpcCaller CheckAPI(); } + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } - public bool APIAvailable { get; private set; } = false; + try + { + return _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 1 } + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query SimpleHeels API version"); + return IpcConnectionState.Error; + } + } private void HeelsOffsetChange(string offset) { @@ -74,20 +96,14 @@ public sealed class IpcCallerHeels : IIpcCaller }).ConfigureAwait(false); } - public void CheckAPI() + protected override void Dispose(bool disposing) { - try + base.Dispose(disposing); + if (!disposing) { - APIAvailable = _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 1 }; + return; } - catch - { - APIAvailable = false; - } - } - public void Dispose() - { _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerHonorific.cs b/LightlessSync/Interop/Ipc/IpcCallerHonorific.cs index 58588c5..4a2ed5d 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerHonorific.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerHonorific.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; @@ -8,8 +9,10 @@ using System.Text; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerHonorific : IIpcCaller +public sealed class IpcCallerHonorific : IpcServiceBase { + private static readonly IpcServiceDescriptor HonorificDescriptor = new("Honorific", "Honorific", new Version(0, 0, 0, 0)); + private readonly ICallGateSubscriber<(uint major, uint minor)> _honorificApiVersion; private readonly ICallGateSubscriber _honorificClearCharacterTitle; private readonly ICallGateSubscriber _honorificDisposing; @@ -22,7 +25,7 @@ public sealed class IpcCallerHonorific : IIpcCaller private readonly DalamudUtilService _dalamudUtil; public IpcCallerHonorific(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, - LightlessMediator lightlessMediator) + LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, HonorificDescriptor) { _logger = logger; _lightlessMediator = lightlessMediator; @@ -41,23 +44,14 @@ public sealed class IpcCallerHonorific : IIpcCaller CheckAPI(); } - - public bool APIAvailable { get; private set; } = false; - - public void CheckAPI() + protected override void Dispose(bool disposing) { - try + base.Dispose(disposing); + if (!disposing) { - APIAvailable = _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 1 }; + return; } - catch - { - APIAvailable = false; - } - } - public void Dispose() - { _honorificLocalCharacterTitleChanged.Unsubscribe(OnHonorificLocalCharacterTitleChanged); _honorificDisposing.Unsubscribe(OnHonorificDisposing); _honorificReady.Unsubscribe(OnHonorificReady); @@ -113,6 +107,27 @@ public sealed class IpcCallerHonorific : IIpcCaller } } + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + return _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 1 } + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Honorific API version"); + return IpcConnectionState.Error; + } + } + private void OnHonorificDisposing() { _lightlessMediator.Publish(new HonorificMessage(string.Empty)); diff --git a/LightlessSync/Interop/Ipc/IpcCallerMoodles.cs b/LightlessSync/Interop/Ipc/IpcCallerMoodles.cs index 610ece4..1bbbfda 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerMoodles.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerMoodles.cs @@ -1,16 +1,18 @@ -using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Plugin; +using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerMoodles : IIpcCaller +public sealed class IpcCallerMoodles : IpcServiceBase { + private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0)); + private readonly ICallGateSubscriber _moodlesApiVersion; - private readonly ICallGateSubscriber _moodlesOnChange; + private readonly ICallGateSubscriber _moodlesOnChange; private readonly ICallGateSubscriber _moodlesGetStatus; private readonly ICallGateSubscriber _moodlesSetStatus; private readonly ICallGateSubscriber _moodlesRevertStatus; @@ -19,14 +21,14 @@ public sealed class IpcCallerMoodles : IIpcCaller private readonly LightlessMediator _lightlessMediator; public IpcCallerMoodles(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, - LightlessMediator lightlessMediator) + LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, MoodlesDescriptor) { _logger = logger; _dalamudUtil = dalamudUtil; _lightlessMediator = lightlessMediator; _moodlesApiVersion = pi.GetIpcSubscriber("Moodles.Version"); - _moodlesOnChange = pi.GetIpcSubscriber("Moodles.StatusManagerModified"); + _moodlesOnChange = pi.GetIpcSubscriber("Moodles.StatusManagerModified"); _moodlesGetStatus = pi.GetIpcSubscriber("Moodles.GetStatusManagerByPtrV2"); _moodlesSetStatus = pi.GetIpcSubscriber("Moodles.SetStatusManagerByPtrV2"); _moodlesRevertStatus = pi.GetIpcSubscriber("Moodles.ClearStatusManagerByPtrV2"); @@ -36,27 +38,19 @@ public sealed class IpcCallerMoodles : IIpcCaller CheckAPI(); } - private void OnMoodlesChange(IPlayerCharacter character) + private void OnMoodlesChange(nint address) { - _lightlessMediator.Publish(new MoodlesMessage(character.Address)); + _lightlessMediator.Publish(new MoodlesMessage(address)); } - public bool APIAvailable { get; private set; } = false; - - public void CheckAPI() + protected override void Dispose(bool disposing) { - try + base.Dispose(disposing); + if (!disposing) { - APIAvailable = _moodlesApiVersion.InvokeFunc() == 3; + return; } - catch - { - APIAvailable = false; - } - } - public void Dispose() - { _moodlesOnChange.Unsubscribe(OnMoodlesChange); } @@ -101,4 +95,25 @@ public sealed class IpcCallerMoodles : IIpcCaller _logger.LogWarning(e, "Could not Set Moodles Status"); } } + + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + return _moodlesApiVersion.InvokeFunc() >= 4 + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Moodles API version"); + return IpcConnectionState.Error; + } + } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs b/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs index fd92fca..c167654 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs @@ -1,146 +1,205 @@ -using Dalamud.Plugin; +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.Interop.Ipc.Penumbra; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; +using LightlessSync.Services.ActorTracking; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Api.IpcSubscribers; -using System.Collections.Concurrent; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCaller +public sealed class IpcCallerPenumbra : IpcServiceBase { - private readonly IDalamudPluginInterface _pi; - private readonly DalamudUtilService _dalamudUtil; - private readonly LightlessMediator _lightlessMediator; - private readonly RedrawManager _redrawManager; - private bool _shownPenumbraUnavailable = false; - private string? _penumbraModDirectory; - public string? ModDirectory - { - get => _penumbraModDirectory; - private set - { - if (!string.Equals(_penumbraModDirectory, value, StringComparison.Ordinal)) - { - _penumbraModDirectory = value; - _lightlessMediator.Publish(new PenumbraDirectoryChangedMessage(_penumbraModDirectory)); - } - } - } + private static readonly IpcServiceDescriptor PenumbraDescriptor = new("Penumbra", "Penumbra", new Version(1, 2, 0, 22)); - private readonly ConcurrentDictionary _penumbraRedrawRequests = new(); + private readonly PenumbraCollections _collections; + private readonly PenumbraResource _resources; + private readonly PenumbraRedraw _redraw; + private readonly PenumbraTexture _textures; - private readonly EventSubscriber _penumbraDispose; - private readonly EventSubscriber _penumbraGameObjectResourcePathResolved; - private readonly EventSubscriber _penumbraInit; - private readonly EventSubscriber _penumbraModSettingChanged; - private readonly EventSubscriber _penumbraObjectIsRedrawn; - - private readonly AddTemporaryMod _penumbraAddTemporaryMod; - private readonly AssignTemporaryCollection _penumbraAssignTemporaryCollection; - private readonly ConvertTextureFile _penumbraConvertTextureFile; - private readonly CreateTemporaryCollection _penumbraCreateNamedTemporaryCollection; private readonly GetEnabledState _penumbraEnabled; - private readonly GetPlayerMetaManipulations _penumbraGetMetaManipulations; - private readonly RedrawObject _penumbraRedraw; - private readonly DeleteTemporaryCollection _penumbraRemoveTemporaryCollection; - private readonly RemoveTemporaryMod _penumbraRemoveTemporaryMod; - private readonly GetModDirectory _penumbraResolveModDir; - private readonly ResolvePlayerPathsAsync _penumbraResolvePaths; - private readonly GetGameObjectResourcePaths _penumbraResourcePaths; + private readonly GetModDirectory _penumbraGetModDirectory; + private readonly EventSubscriber _penumbraInit; + private readonly EventSubscriber _penumbraDispose; + private readonly EventSubscriber _penumbraModSettingChanged; - public IpcCallerPenumbra(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, - LightlessMediator lightlessMediator, RedrawManager redrawManager) : base(logger, lightlessMediator) + private bool _shownPenumbraUnavailable; + private string? _modDirectory; + + public IpcCallerPenumbra( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator, + RedrawManager redrawManager, + ActorObjectService actorObjectService) : base(logger, mediator, pluginInterface, PenumbraDescriptor) { - _pi = pi; - _dalamudUtil = dalamudUtil; - _lightlessMediator = lightlessMediator; - _redrawManager = redrawManager; - _penumbraInit = Initialized.Subscriber(pi, PenumbraInit); - _penumbraDispose = Disposed.Subscriber(pi, PenumbraDispose); - _penumbraResolveModDir = new GetModDirectory(pi); - _penumbraRedraw = new RedrawObject(pi); - _penumbraObjectIsRedrawn = GameObjectRedrawn.Subscriber(pi, RedrawEvent); - _penumbraGetMetaManipulations = new GetPlayerMetaManipulations(pi); - _penumbraRemoveTemporaryMod = new RemoveTemporaryMod(pi); - _penumbraAddTemporaryMod = new AddTemporaryMod(pi); - _penumbraCreateNamedTemporaryCollection = new CreateTemporaryCollection(pi); - _penumbraRemoveTemporaryCollection = new DeleteTemporaryCollection(pi); - _penumbraAssignTemporaryCollection = new AssignTemporaryCollection(pi); - _penumbraResolvePaths = new ResolvePlayerPathsAsync(pi); - _penumbraEnabled = new GetEnabledState(pi); - _penumbraModSettingChanged = ModSettingChanged.Subscriber(pi, (change, arg1, arg, b) => - { - if (change == ModSettingChange.EnableState) - _lightlessMediator.Publish(new PenumbraModSettingChangedMessage()); - }); - _penumbraConvertTextureFile = new ConvertTextureFile(pi); - _penumbraResourcePaths = new GetGameObjectResourcePaths(pi); + _penumbraEnabled = new GetEnabledState(pluginInterface); + _penumbraGetModDirectory = new GetModDirectory(pluginInterface); + _penumbraInit = Initialized.Subscriber(pluginInterface, HandlePenumbraInitialized); + _penumbraDispose = Disposed.Subscriber(pluginInterface, HandlePenumbraDisposed); + _penumbraModSettingChanged = ModSettingChanged.Subscriber(pluginInterface, HandlePenumbraModSettingChanged); - _penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded); + _collections = RegisterInterop(new PenumbraCollections(logger, pluginInterface, dalamudUtil, mediator)); + _resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator, actorObjectService)); + _redraw = RegisterInterop(new PenumbraRedraw(logger, pluginInterface, dalamudUtil, mediator, redrawManager)); + _textures = RegisterInterop(new PenumbraTexture(logger, pluginInterface, dalamudUtil, mediator, _redraw)); + + SubscribeMediatorEvents(); CheckAPI(); CheckModDirectory(); - - Mediator.Subscribe(this, (msg) => - { - _penumbraRedraw.Invoke(msg.Character.ObjectIndex, RedrawType.AfterGPose); - }); - - Mediator.Subscribe(this, (msg) => _shownPenumbraUnavailable = false); } - public bool APIAvailable { get; private set; } = false; - - public void CheckAPI() + public string? ModDirectory { - bool penumbraAvailable = false; - try + get => _modDirectory; + private set { - var penumbraVersion = (_pi.InstalledPlugins - .FirstOrDefault(p => string.Equals(p.InternalName, "Penumbra", StringComparison.OrdinalIgnoreCase)) - ?.Version ?? new Version(0, 0, 0, 0)); - penumbraAvailable = penumbraVersion >= new Version(1, 2, 0, 22); - try + if (string.Equals(_modDirectory, value, StringComparison.Ordinal)) { - penumbraAvailable &= _penumbraEnabled.Invoke(); - } - catch - { - penumbraAvailable = false; - } - _shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable; - APIAvailable = penumbraAvailable; - } - catch - { - APIAvailable = penumbraAvailable; - } - finally - { - if (!penumbraAvailable && !_shownPenumbraUnavailable) - { - _shownPenumbraUnavailable = true; - _lightlessMediator.Publish(new NotificationMessage("Penumbra inactive", - "Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Lightless. If you just updated Penumbra, ignore this message.", - NotificationType.Error)); + return; } + + _modDirectory = value; + Mediator.Publish(new PenumbraDirectoryChangedMessage(_modDirectory)); } } + public Task AssignTemporaryCollectionAsync(ILogger logger, Guid collectionId, int objectIndex) + => _collections.AssignTemporaryCollectionAsync(logger, collectionId, objectIndex); + + public Task CreateTemporaryCollectionAsync(ILogger logger, string uid) + => _collections.CreateTemporaryCollectionAsync(logger, uid); + + public Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collectionId) + => _collections.RemoveTemporaryCollectionAsync(logger, applicationId, collectionId); + + public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary modPaths) + => _collections.SetTemporaryModsAsync(logger, applicationId, collectionId, modPaths); + + public Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData) + => _collections.SetManipulationDataAsync(logger, applicationId, collectionId, manipulationData); + + public Task>?> GetCharacterData(ILogger logger, GameObjectHandler handler) + => _resources.GetCharacterDataAsync(logger, handler); + + public string GetMetaManipulations() + => _resources.GetMetaManipulations(); + + public Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse) + => _resources.ResolvePathsAsync(forward, reverse); + + public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) + => _redraw.RedrawAsync(logger, handler, applicationId, token); + + public Task ConvertTextureFiles(ILogger logger, IReadOnlyList jobs, IProgress? progress, CancellationToken token) + => _textures.ConvertTextureFilesAsync(logger, jobs, progress, token); + + public Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token) + => _textures.ConvertTextureFileDirectAsync(job, token); + public void CheckModDirectory() { if (!APIAvailable) { ModDirectory = string.Empty; + return; } - else + + try { - ModDirectory = _penumbraResolveModDir!.Invoke().ToLowerInvariant(); + ModDirectory = _penumbraGetModDirectory.Invoke().ToLowerInvariant(); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to resolve Penumbra mod directory"); + } + } + + protected override bool IsPluginEnabled(IExposedPlugin plugin) + { + try + { + return _penumbraEnabled.Invoke(); + } + catch + { + return false; + } + } + + protected override void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current) + { + base.OnConnectionStateChanged(previous, current); + + if (current == IpcConnectionState.Available) + { + _shownPenumbraUnavailable = false; + if (string.IsNullOrEmpty(ModDirectory)) + { + CheckModDirectory(); + } + return; + } + + ModDirectory = string.Empty; + _redraw.CancelPendingRedraws(); + + if (_shownPenumbraUnavailable || current == IpcConnectionState.Unknown) + { + return; + } + + _shownPenumbraUnavailable = true; + Mediator.Publish(new NotificationMessage( + "Penumbra inactive", + "Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Lightless. If you just updated Penumbra, ignore this message.", + NotificationType.Error)); + } + + private void SubscribeMediatorEvents() + { + Mediator.Subscribe(this, msg => + { + _redraw.RequestImmediateRedraw(msg.Character.ObjectIndex, RedrawType.AfterGPose); + }); + + Mediator.Subscribe(this, _ => _shownPenumbraUnavailable = false); + + Mediator.Subscribe(this, msg => _resources.TrackActor(msg.Descriptor.Address)); + Mediator.Subscribe(this, msg => _resources.UntrackActor(msg.Descriptor.Address)); + Mediator.Subscribe(this, msg => _resources.TrackActor(msg.GameObjectHandler.Address)); + Mediator.Subscribe(this, msg => _resources.UntrackActor(msg.GameObjectHandler.Address)); + } + + private void HandlePenumbraInitialized() + { + Mediator.Publish(new PenumbraInitializedMessage()); + CheckModDirectory(); + _redraw.RequestImmediateRedraw(0, RedrawType.Redraw); + CheckAPI(); + } + + private void HandlePenumbraDisposed() + { + _redraw.CancelPendingRedraws(); + ModDirectory = string.Empty; + Mediator.Publish(new PenumbraDisposedMessage()); + CheckAPI(); + } + + private void HandlePenumbraModSettingChanged(ModSettingChange change, Guid _, string __, bool ___) + { + if (change == ModSettingChange.EnableState) + { + Mediator.Publish(new PenumbraModSettingChangedMessage()); + CheckAPI(); } } @@ -148,196 +207,13 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa { base.Dispose(disposing); - _redrawManager.Cancel(); + if (!disposing) + { + return; + } _penumbraModSettingChanged.Dispose(); - _penumbraGameObjectResourcePathResolved.Dispose(); _penumbraDispose.Dispose(); _penumbraInit.Dispose(); - _penumbraObjectIsRedrawn.Dispose(); - } - - public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collName, int idx) - { - if (!APIAvailable) return; - - await _dalamudUtil.RunOnFrameworkThread(() => - { - var retAssign = _penumbraAssignTemporaryCollection.Invoke(collName, idx, forceAssignment: true); - logger.LogTrace("Assigning Temp Collection {collName} to index {idx}, Success: {ret}", collName, idx, retAssign); - return collName; - }).ConfigureAwait(false); - } - - public async Task ConvertTextureFiles(ILogger logger, Dictionary 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 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>?> 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 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); } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerPetNames.cs b/LightlessSync/Interop/Ipc/IpcCallerPetNames.cs index 9839f29..5d7fea9 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerPetNames.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerPetNames.cs @@ -1,14 +1,17 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerPetNames : IIpcCaller +public sealed class IpcCallerPetNames : IpcServiceBase { + private static readonly IpcServiceDescriptor PetRenamerDescriptor = new("PetRenamer", "Pet Renamer", new Version(0, 0, 0, 0)); + private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtil; private readonly LightlessMediator _lightlessMediator; @@ -24,7 +27,7 @@ public sealed class IpcCallerPetNames : IIpcCaller private readonly ICallGateSubscriber _clearPlayerData; public IpcCallerPetNames(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, - LightlessMediator lightlessMediator) + LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, PetRenamerDescriptor) { _logger = logger; _dalamudUtil = dalamudUtil; @@ -46,25 +49,6 @@ public sealed class IpcCallerPetNames : IIpcCaller CheckAPI(); } - - public bool APIAvailable { get; private set; } = false; - - public void CheckAPI() - { - try - { - APIAvailable = _enabled?.InvokeFunc() ?? false; - if (APIAvailable) - { - APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 4, Item2: >= 0 }; - } - } - catch - { - APIAvailable = false; - } - } - private void OnPetNicknamesReady() { CheckAPI(); @@ -76,6 +60,34 @@ public sealed class IpcCallerPetNames : IIpcCaller _lightlessMediator.Publish(new PetNamesMessage(string.Empty)); } + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + var enabled = _enabled?.InvokeFunc() ?? false; + if (!enabled) + { + return IpcConnectionState.PluginDisabled; + } + + var version = _apiVersion?.InvokeFunc() ?? (0u, 0u); + return version.Item1 == 4 && version.Item2 >= 0 + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Pet Renamer API version"); + return IpcConnectionState.Error; + } + } + public string GetLocalNames() { if (!APIAvailable) return string.Empty; @@ -149,8 +161,14 @@ public sealed class IpcCallerPetNames : IIpcCaller _lightlessMediator.Publish(new PetNamesMessage(data)); } - public void Dispose() + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + if (!disposing) + { + return; + } + _petnamesReady.Unsubscribe(OnPetNicknamesReady); _petnamesDisposing.Unsubscribe(OnPetNicknamesDispose); _playerDataChanged.Unsubscribe(OnLocalPetNicknamesDataChange); diff --git a/LightlessSync/Interop/Ipc/IpcProvider.cs b/LightlessSync/Interop/Ipc/IpcProvider.cs index 88e0202..77f8043 100644 --- a/LightlessSync/Interop/Ipc/IpcProvider.cs +++ b/LightlessSync/Interop/Ipc/IpcProvider.cs @@ -1,4 +1,5 @@ -using Dalamud.Game.ClientState.Objects.Types; +using System; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using LightlessSync.PlayerData.Handlers; @@ -14,9 +15,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber private readonly ILogger _logger; private readonly IDalamudPluginInterface _pi; private readonly CharaDataManager _charaDataManager; - private ICallGateProvider? _loadFileProvider; - private ICallGateProvider>? _loadFileAsyncProvider; - private ICallGateProvider>? _handledGameAddresses; + private readonly List _ipcRegisters = []; private readonly List _activeGameObjectHandlers = []; public LightlessMediator Mediator { get; init; } @@ -44,12 +43,9 @@ public class IpcProvider : IHostedService, IMediatorSubscriber public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting IpcProviderService"); - _loadFileProvider = _pi.GetIpcProvider("LightlessSync.LoadMcdf"); - _loadFileProvider.RegisterFunc(LoadMcdf); - _loadFileAsyncProvider = _pi.GetIpcProvider>("LightlessSync.LoadMcdfAsync"); - _loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync); - _handledGameAddresses = _pi.GetIpcProvider>("LightlessSync.GetHandledAddresses"); - _handledGameAddresses.RegisterFunc(GetHandledAddresses); + _ipcRegisters.Add(RegisterFunc("LightlessSync.LoadMcdf", LoadMcdf)); + _ipcRegisters.Add(RegisterFunc>("LightlessSync.LoadMcdfAsync", LoadMcdfAsync)); + _ipcRegisters.Add(RegisterFunc("LightlessSync.GetHandledAddresses", GetHandledAddresses)); _logger.LogInformation("Started IpcProviderService"); return Task.CompletedTask; } @@ -57,9 +53,11 @@ public class IpcProvider : IHostedService, IMediatorSubscriber public Task StopAsync(CancellationToken cancellationToken) { _logger.LogDebug("Stopping IpcProvider Service"); - _loadFileProvider?.UnregisterFunc(); - _loadFileAsyncProvider?.UnregisterFunc(); - _handledGameAddresses?.UnregisterFunc(); + foreach (var register in _ipcRegisters) + { + register.Dispose(); + } + _ipcRegisters.Clear(); Mediator.UnsubscribeAll(this); return Task.CompletedTask; } @@ -89,4 +87,40 @@ public class IpcProvider : IHostedService, IMediatorSubscriber { return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList(); } + + private IpcRegister RegisterFunc(string label, Func> handler) + { + var provider = _pi.GetIpcProvider>(label); + provider.RegisterFunc(handler); + return new IpcRegister(provider.UnregisterFunc); + } + + private IpcRegister RegisterFunc(string label, Func handler) + { + var provider = _pi.GetIpcProvider(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; + } + } } diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs new file mode 100644 index 0000000..4f7b000 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs @@ -0,0 +1,27 @@ +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public abstract class PenumbraBase : IpcInteropBase +{ + protected PenumbraBase( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator) : base(logger) + { + PluginInterface = pluginInterface; + DalamudUtil = dalamudUtil; + Mediator = mediator; + } + + protected IDalamudPluginInterface PluginInterface { get; } + + protected DalamudUtilService DalamudUtil { get; } + + protected LightlessMediator Mediator { get; } +} diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs new file mode 100644 index 0000000..e5c28e2 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs @@ -0,0 +1,197 @@ +using System.Collections.Concurrent; +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public sealed class PenumbraCollections : PenumbraBase +{ + private readonly CreateTemporaryCollection _createNamedTemporaryCollection; + private readonly AssignTemporaryCollection _assignTemporaryCollection; + private readonly DeleteTemporaryCollection _removeTemporaryCollection; + private readonly AddTemporaryMod _addTemporaryMod; + private readonly RemoveTemporaryMod _removeTemporaryMod; + private readonly GetCollections _getCollections; + private readonly ConcurrentDictionary _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 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 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(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); +} diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs new file mode 100644 index 0000000..5d47d3a --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs @@ -0,0 +1,89 @@ +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.PlayerData.Handlers; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public sealed class PenumbraRedraw : PenumbraBase +{ + private readonly RedrawManager _redrawManager; + private readonly RedrawObject _penumbraRedraw; + private readonly EventSubscriber _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(); + } +} diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs new file mode 100644 index 0000000..75d1d86 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs @@ -0,0 +1,141 @@ +using System.Collections.Concurrent; +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.PlayerData.Handlers; +using LightlessSync.Services; +using LightlessSync.Services.ActorTracking; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public sealed class PenumbraResource : PenumbraBase +{ + private readonly ActorObjectService _actorObjectService; + private readonly GetGameObjectResourcePaths _gameObjectResourcePaths; + private readonly ResolvePlayerPathsAsync _resolvePlayerPaths; + private readonly GetPlayerMetaManipulations _getPlayerMetaManipulations; + private readonly EventSubscriber _gameObjectResourcePathResolved; + private readonly ConcurrentDictionary _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>?> 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(), Array.Empty()); + } + + 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(); + } +} diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs new file mode 100644 index 0000000..e12fd7b --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs @@ -0,0 +1,121 @@ +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public sealed class PenumbraTexture : PenumbraBase +{ + private readonly PenumbraRedraw _redrawFeature; + private readonly ConvertTextureFile _convertTextureFile; + + public PenumbraTexture( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator, + PenumbraRedraw redrawFeature) : base(logger, pluginInterface, dalamudUtil, mediator) + { + _redrawFeature = redrawFeature; + _convertTextureFile = new ConvertTextureFile(pluginInterface); + } + + public override string Name => "Penumbra.Textures"; + + public async Task ConvertTextureFilesAsync(ILogger logger, IReadOnlyList jobs, IProgress? 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) + { + } +} diff --git a/LightlessSync/Interop/Ipc/TextureConversionJob.cs b/LightlessSync/Interop/Ipc/TextureConversionJob.cs new file mode 100644 index 0000000..2bbe9fd --- /dev/null +++ b/LightlessSync/Interop/Ipc/TextureConversionJob.cs @@ -0,0 +1,21 @@ +using Penumbra.Api.Enums; + +namespace LightlessSync.Interop.Ipc; + +/// +/// Represents a single texture conversion request, including optional duplicate targets. +/// +public sealed record TextureConversionJob( + string InputFile, + string OutputFile, + TextureType TargetType, + bool IncludeMipMaps = true, + IReadOnlyList? DuplicateTargets = null); + +/// +/// Progress payload for a texture conversion batch. +/// +/// Number of completed conversions. +/// Total number of conversions scheduled. +/// The job currently being processed. +public sealed record TextureConversionProgress(int Completed, int Total, TextureConversionJob CurrentJob); diff --git a/LightlessSync/LightlessConfiguration/ChatConfigService.cs b/LightlessSync/LightlessConfiguration/ChatConfigService.cs new file mode 100644 index 0000000..f91cfca --- /dev/null +++ b/LightlessSync/LightlessConfiguration/ChatConfigService.cs @@ -0,0 +1,14 @@ +using LightlessSync.LightlessConfiguration.Configurations; + +namespace LightlessSync.LightlessConfiguration; + +public sealed class ChatConfigService : ConfigurationServiceBase +{ + public const string ConfigName = "chatconfig.json"; + + public ChatConfigService(string configDir) : base(configDir) + { + } + + public override string ConfigurationName => ConfigName; +} diff --git a/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs new file mode 100644 index 0000000..f438c45 --- /dev/null +++ b/LightlessSync/LightlessConfiguration/Configurations/ChatConfig.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace LightlessSync.LightlessConfiguration.Configurations; + +[Serializable] +public sealed class ChatConfig : ILightlessConfiguration +{ + public int Version { get; set; } = 1; + public bool AutoEnableChatOnLogin { get; set; } = false; + public bool ShowRulesOverlayOnOpen { get; set; } = true; + public bool ShowMessageTimestamps { get; set; } = true; + public float ChatWindowOpacity { get; set; } = .97f; + public bool FadeWhenUnfocused { get; set; } = false; + public float UnfocusedWindowOpacity { get; set; } = 0.6f; + public bool IsWindowPinned { get; set; } = false; + public bool AutoOpenChatOnPluginLoad { get; set; } = false; + public float ChatFontScale { get; set; } = 1.0f; + public bool HideInCombat { get; set; } = false; + public bool HideInDuty { get; set; } = false; + public bool ShowWhenUiHidden { get; set; } = true; + public bool ShowInCutscenes { get; set; } = true; + public bool ShowInGpose { get; set; } = true; + public List ChannelOrder { get; set; } = new(); + public Dictionary PreferNotesForChannels { get; set; } = new(StringComparer.Ordinal); +} diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index 929cbbc..9b4055b 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -2,6 +2,7 @@ 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; @@ -48,6 +49,8 @@ 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.Alphabetical; + public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical; public float ProfileDelay { get; set; } = 1.5f; public bool ProfilePopoutRight { get; set; } = false; public bool ProfilesAllowNsfw { get; set; } = false; @@ -61,8 +64,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; diff --git a/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs index ca12006..7da9ac2 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs @@ -4,6 +4,7 @@ public class PlayerPerformanceConfig : ILightlessConfiguration { public int Version { get; set; } = 1; public bool ShowPerformanceIndicator { get; set; } = true; + public bool ShowPerformanceUsageNextToName { get; set; } = false; public bool WarnOnExceedingThresholds { get; set; } = true; public bool WarnOnPreferredPermissionsExceedingThresholds { get; set; } = false; public int VRAMSizeWarningThresholdMiB { get; set; } = 375; @@ -16,4 +17,9 @@ 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; } \ No newline at end of file diff --git a/LightlessSync/LightlessConfiguration/Configurations/TransientConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/TransientConfig.cs index c9a5f74..0bcb5ad 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/TransientConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/TransientConfig.cs @@ -13,6 +13,8 @@ public class TransientConfig : ILightlessConfiguration public Dictionary> JobSpecificCache { get; set; } = []; public Dictionary> JobSpecificPetCache { get; set; } = []; + private readonly object _cacheLock = new(); + public TransientPlayerConfig() { @@ -39,45 +41,51 @@ public class TransientConfig : ILightlessConfiguration public int RemovePath(string gamePath, ObjectKind objectKind) { - int removedEntries = 0; - if (objectKind == ObjectKind.Player) + lock (_cacheLock) { - if (GlobalPersistentCache.Remove(gamePath)) removedEntries++; - foreach (var kvp in JobSpecificCache) + int removedEntries = 0; + if (objectKind == ObjectKind.Player) { - if (kvp.Value.Remove(gamePath)) removedEntries++; + if (GlobalPersistentCache.Remove(gamePath)) removedEntries++; + foreach (var kvp in JobSpecificCache) + { + if (kvp.Value.Remove(gamePath)) removedEntries++; + } } - } - if (objectKind == ObjectKind.Pet) - { - foreach (var kvp in JobSpecificPetCache) + if (objectKind == ObjectKind.Pet) { - if (kvp.Value.Remove(gamePath)) removedEntries++; + foreach (var kvp in JobSpecificPetCache) + { + if (kvp.Value.Remove(gamePath)) removedEntries++; + } } + return removedEntries; } - return removedEntries; } public void AddOrElevate(uint jobId, string gamePath) { - // check if it's in the global cache, if yes, do nothing - if (GlobalPersistentCache.Contains(gamePath, StringComparer.Ordinal)) + lock (_cacheLock) { - return; - } + // check if it's in the global cache, if yes, do nothing + if (GlobalPersistentCache.Contains(gamePath, StringComparer.Ordinal)) + { + return; + } - if (ElevateIfNeeded(jobId, gamePath)) return; + if (ElevateIfNeeded(jobId, gamePath)) return; - // check if the jobid is already in the cache to start - if (!JobSpecificCache.TryGetValue(jobId, out var jobCache)) - { - JobSpecificCache[jobId] = jobCache = new(); - } + // check if the jobid is already in the cache to start + if (!JobSpecificCache.TryGetValue(jobId, out var jobCache)) + { + JobSpecificCache[jobId] = jobCache = new(); + } - // check if the path is already in the job specific cache - if (!jobCache.Contains(gamePath, StringComparer.Ordinal)) - { - jobCache.Add(gamePath); + // check if the path is already in the job specific cache + if (!jobCache.Contains(gamePath, StringComparer.Ordinal)) + { + jobCache.Add(gamePath); + } } } } diff --git a/LightlessSync/LightlessConfiguration/Models/Obsolete/ServerStorageV0.cs b/LightlessSync/LightlessConfiguration/Models/Obsolete/ServerStorageV0.cs deleted file mode 100644 index 0cb5f3e..0000000 --- a/LightlessSync/LightlessConfiguration/Models/Obsolete/ServerStorageV0.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace LightlessSync.LightlessConfiguration.Models.Obsolete; - -[Serializable] -[Obsolete("Deprecated, use ServerStorage")] -public class ServerStorageV0 -{ - public List Authentications { get; set; } = []; - public bool FullPause { get; set; } = false; - public Dictionary GidServerComments { get; set; } = new(StringComparer.Ordinal); - public HashSet OpenPairTags { get; set; } = new(StringComparer.Ordinal); - public Dictionary SecretKeys { get; set; } = []; - public HashSet ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal); - public string ServerName { get; set; } = string.Empty; - public string ServerUri { get; set; } = string.Empty; - public Dictionary UidServerComments { get; set; } = new(StringComparer.Ordinal); - public Dictionary> 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) - }; - } -} \ No newline at end of file diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 726f2ef..28e7eb2 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -1,16 +1,16 @@ - + - 1.12.4 + 2.0.0 https://github.com/Light-Public-Syncshells/LightlessClient - net9.0-windows7.0 + net10.0-windows7.0 x64 enable latest @@ -27,25 +27,25 @@ - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -77,7 +77,28 @@ - + + + + + + + + + lib\OtterTex.dll + true + + + + + + PreserveNewest + DirectXTexC.dll + + + + + diff --git a/LightlessSync/PlayerData/Data/FileReplacementDataComparer.cs b/LightlessSync/PlayerData/Data/FileReplacementDataComparer.cs index 7b52b49..5ba716b 100644 --- a/LightlessSync/PlayerData/Data/FileReplacementDataComparer.cs +++ b/LightlessSync/PlayerData/Data/FileReplacementDataComparer.cs @@ -1,4 +1,7 @@ -using LightlessSync.API.Data; +using LightlessSync.API.Data; +using System; +using System.Collections.Generic; +using System.Linq; namespace LightlessSync.PlayerData.Data; @@ -13,37 +16,42 @@ public class FileReplacementDataComparer : IEqualityComparer list1, HashSet list2) + private static bool ComparePathSets(IEnumerable first, IEnumerable second) { - if (list1.Count != list2.Count) - return false; - - for (int i = 0; i < list1.Count; i++) - { - if (!string.Equals(list1.ElementAt(i), list2.ElementAt(i), StringComparison.OrdinalIgnoreCase)) - return false; - } - - return true; + var left = new HashSet(first ?? Enumerable.Empty(), StringComparer.OrdinalIgnoreCase); + var right = new HashSet(second ?? Enumerable.Empty(), StringComparer.OrdinalIgnoreCase); + return left.SetEquals(right); } - private static int GetOrderIndependentHashCode(IEnumerable source) where T : notnull + private static int GetSetHashCode(IEnumerable paths) { int hash = 0; - foreach (T element in source) + foreach (var element in paths ?? Enumerable.Empty()) { - hash = unchecked(hash + - EqualityComparer.Default.GetHashCode(element)); + hash = unchecked(hash + StringComparer.OrdinalIgnoreCase.GetHashCode(element)); } + return hash; } } \ No newline at end of file diff --git a/LightlessSync/PlayerData/Factories/FileDownloadManagerFactory.cs b/LightlessSync/PlayerData/Factories/FileDownloadManagerFactory.cs index 231ded3..e3697cf 100644 --- a/LightlessSync/PlayerData/Factories/FileDownloadManagerFactory.cs +++ b/LightlessSync/PlayerData/Factories/FileDownloadManagerFactory.cs @@ -1,7 +1,7 @@ using LightlessSync.FileCache; using LightlessSync.LightlessConfiguration; -using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Services.TextureCompression; using LightlessSync.WebAPI.Files; using Microsoft.Extensions.Logging; @@ -9,13 +9,14 @@ namespace LightlessSync.PlayerData.Factories; public class FileDownloadManagerFactory { - private readonly FileCacheManager _fileCacheManager; - private readonly FileCompactor _fileCompactor; - private readonly FileTransferOrchestrator _fileTransferOrchestrator; - private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly ILoggerFactory _loggerFactory; private readonly LightlessMediator _lightlessMediator; + private readonly FileTransferOrchestrator _fileTransferOrchestrator; + private readonly FileCacheManager _fileCacheManager; + private readonly FileCompactor _fileCompactor; private readonly LightlessConfigService _configService; + private readonly TextureDownscaleService _textureDownscaleService; + private readonly TextureMetadataHelper _textureMetadataHelper; public FileDownloadManagerFactory( ILoggerFactory loggerFactory, @@ -23,16 +24,18 @@ public class FileDownloadManagerFactory FileTransferOrchestrator fileTransferOrchestrator, FileCacheManager fileCacheManager, FileCompactor fileCompactor, - PairProcessingLimiter pairProcessingLimiter, - LightlessConfigService configService) + LightlessConfigService configService, + TextureDownscaleService textureDownscaleService, + TextureMetadataHelper textureMetadataHelper) { _loggerFactory = loggerFactory; _lightlessMediator = lightlessMediator; _fileTransferOrchestrator = fileTransferOrchestrator; _fileCacheManager = fileCacheManager; _fileCompactor = fileCompactor; - _pairProcessingLimiter = pairProcessingLimiter; _configService = configService; + _textureDownscaleService = textureDownscaleService; + _textureMetadataHelper = textureMetadataHelper; } public FileDownloadManager Create() @@ -43,7 +46,8 @@ public class FileDownloadManagerFactory _fileTransferOrchestrator, _fileCacheManager, _fileCompactor, - _pairProcessingLimiter, - _configService); + _configService, + _textureDownscaleService, + _textureMetadataHelper); } } diff --git a/LightlessSync/PlayerData/Factories/GameObjectHandlerFactory.cs b/LightlessSync/PlayerData/Factories/GameObjectHandlerFactory.cs index 83a7ce6..4741b55 100644 --- a/LightlessSync/PlayerData/Factories/GameObjectHandlerFactory.cs +++ b/LightlessSync/PlayerData/Factories/GameObjectHandlerFactory.cs @@ -2,29 +2,40 @@ using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace LightlessSync.PlayerData.Factories; public class GameObjectHandlerFactory { - private readonly DalamudUtilService _dalamudUtilService; + private readonly IServiceProvider _serviceProvider; private readonly ILoggerFactory _loggerFactory; private readonly LightlessMediator _lightlessMediator; private readonly PerformanceCollectorService _performanceCollectorService; - public GameObjectHandlerFactory(ILoggerFactory loggerFactory, PerformanceCollectorService performanceCollectorService, LightlessMediator lightlessMediator, - DalamudUtilService dalamudUtilService) + public GameObjectHandlerFactory( + ILoggerFactory loggerFactory, + PerformanceCollectorService performanceCollectorService, + LightlessMediator lightlessMediator, + IServiceProvider serviceProvider) { _loggerFactory = loggerFactory; _performanceCollectorService = performanceCollectorService; _lightlessMediator = lightlessMediator; - _dalamudUtilService = dalamudUtilService; + _serviceProvider = serviceProvider; } public async Task Create(ObjectKind objectKind, Func getAddressFunc, bool isWatched = false) { - return await _dalamudUtilService.RunOnFrameworkThread(() => new GameObjectHandler(_loggerFactory.CreateLogger(), - _performanceCollectorService, _lightlessMediator, _dalamudUtilService, objectKind, getAddressFunc, isWatched)).ConfigureAwait(false); + var dalamudUtilService = _serviceProvider.GetRequiredService(); + return await dalamudUtilService.RunOnFrameworkThread(() => new GameObjectHandler( + _loggerFactory.CreateLogger(), + _performanceCollectorService, + _lightlessMediator, + dalamudUtilService, + objectKind, + getAddressFunc, + isWatched)).ConfigureAwait(false); } } \ No newline at end of file diff --git a/LightlessSync/PlayerData/Factories/PairFactory.cs b/LightlessSync/PlayerData/Factories/PairFactory.cs index a9ee0ab..a7ffd6e 100644 --- a/LightlessSync/PlayerData/Factories/PairFactory.cs +++ b/LightlessSync/PlayerData/Factories/PairFactory.cs @@ -1,35 +1,83 @@ -using LightlessSync.API.Dto.User; +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Dto.User; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; +using LightlessSync.UI.Models; using Microsoft.Extensions.Logging; +using LightlessSync.WebAPI; namespace LightlessSync.PlayerData.Factories; public class PairFactory { - private readonly PairHandlerFactory _cachedPlayerFactory; + private readonly PairLedger _pairLedger; private readonly ILoggerFactory _loggerFactory; private readonly LightlessMediator _lightlessMediator; - private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly Lazy _serverConfigurationManager; + private readonly Lazy _apiController; - public PairFactory(ILoggerFactory loggerFactory, PairHandlerFactory cachedPlayerFactory, - LightlessMediator lightlessMediator, ServerConfigurationManager serverConfigurationManager) + public PairFactory( + ILoggerFactory loggerFactory, + PairLedger pairLedger, + LightlessMediator lightlessMediator, + Lazy serverConfigurationManager, + Lazy apiController) { _loggerFactory = loggerFactory; - _cachedPlayerFactory = cachedPlayerFactory; + _pairLedger = pairLedger; _lightlessMediator = lightlessMediator; _serverConfigurationManager = serverConfigurationManager; + _apiController = apiController; } public Pair Create(UserFullPairDto userPairDto) { - return new Pair(_loggerFactory.CreateLogger(), userPairDto, _cachedPlayerFactory, _lightlessMediator, _serverConfigurationManager); + return CreateInternal(userPairDto); } public Pair Create(UserPairDto userPairDto) { - return new Pair(_loggerFactory.CreateLogger(), new(userPairDto.User, userPairDto.IndividualPairStatus, [], userPairDto.OwnPermissions, userPairDto.OtherPermissions), - _cachedPlayerFactory, _lightlessMediator, _serverConfigurationManager); + var full = new UserFullPairDto( + userPairDto.User, + userPairDto.IndividualPairStatus, + new List(), + userPairDto.OwnPermissions, + userPairDto.OtherPermissions); + + return CreateInternal(full); } -} \ No newline at end of file + + 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(), + dto, + _pairLedger, + _lightlessMediator, + _serverConfigurationManager.Value, + _apiController); + } +} diff --git a/LightlessSync/PlayerData/Factories/PairHandlerFactory.cs b/LightlessSync/PlayerData/Factories/PairHandlerFactory.cs deleted file mode 100644 index 9cb74da..0000000 --- a/LightlessSync/PlayerData/Factories/PairHandlerFactory.cs +++ /dev/null @@ -1,55 +0,0 @@ -using LightlessSync.FileCache; -using LightlessSync.Interop.Ipc; -using LightlessSync.PlayerData.Handlers; -using LightlessSync.PlayerData.Pairs; -using LightlessSync.Services; -using LightlessSync.Services.Mediator; -using LightlessSync.Services.ServerConfiguration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace LightlessSync.PlayerData.Factories; - -public class PairHandlerFactory -{ - private readonly DalamudUtilService _dalamudUtilService; - private readonly FileCacheManager _fileCacheManager; - private readonly FileDownloadManagerFactory _fileDownloadManagerFactory; - private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; - private readonly IHostApplicationLifetime _hostApplicationLifetime; - private readonly IpcManager _ipcManager; - private readonly ILoggerFactory _loggerFactory; - private readonly LightlessMediator _lightlessMediator; - private readonly PlayerPerformanceService _playerPerformanceService; - private readonly 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(), pair, _gameObjectHandlerFactory, - _ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime, - _fileCacheManager, _lightlessMediator, _playerPerformanceService, _pairProcessingLimiter, _serverConfigManager); - } -} \ No newline at end of file diff --git a/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs b/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs index f752051..39aa6c8 100644 --- a/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs +++ b/LightlessSync/PlayerData/Factories/PlayerDataFactory.cs @@ -119,6 +119,7 @@ public class PlayerDataFactory CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new(); _logger.LogDebug("Building character data for {obj}", playerRelatedObject); + var logDebug = _logger.IsEnabled(LogLevel.Debug); // wait until chara is not drawing and present so nothing spontaneously explodes await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false); @@ -132,11 +133,6 @@ public class PlayerDataFactory ct.ThrowIfCancellationRequested(); - Dictionary>? boneIndices = - objectKind != ObjectKind.Player - ? null - : await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false); - DateTime start = DateTime.UtcNow; // penumbra call, it's currently broken @@ -154,11 +150,21 @@ public class PlayerDataFactory ct.ThrowIfCancellationRequested(); - _logger.LogDebug("== Static Replacements =="); - foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase)) + if (logDebug) { - _logger.LogDebug("=> {repl}", replacement); - ct.ThrowIfCancellationRequested(); + _logger.LogDebug("== Static Replacements =="); + foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase)) + { + _logger.LogDebug("=> {repl}", replacement); + ct.ThrowIfCancellationRequested(); + } + } + else + { + foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement)) + { + ct.ThrowIfCancellationRequested(); + } } await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false); @@ -190,11 +196,21 @@ public class PlayerDataFactory var transientPaths = ManageSemiTransientData(objectKind); var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet(StringComparer.Ordinal)).ConfigureAwait(false); - _logger.LogDebug("== Transient Replacements =="); - foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal)) + if (logDebug) { - _logger.LogDebug("=> {repl}", replacement); - fragment.FileReplacements.Add(replacement); + _logger.LogDebug("== Transient Replacements =="); + foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal)) + { + _logger.LogDebug("=> {repl}", replacement); + fragment.FileReplacements.Add(replacement); + } + } + else + { + foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key))) + { + fragment.FileReplacements.Add(replacement); + } } // clean up all semi transient resources that don't have any file replacement (aka null resolve) @@ -252,11 +268,26 @@ public class PlayerDataFactory ct.ThrowIfCancellationRequested(); + Dictionary>? boneIndices = null; + var hasPapFiles = false; + if (objectKind == ObjectKind.Player) + { + hasPapFiles = fragment.FileReplacements.Any(f => + !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)); + if (hasPapFiles) + { + boneIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false); + } + } + if (objectKind == ObjectKind.Player) { try { - await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false); + if (hasPapFiles) + { + await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false); + } } catch (OperationCanceledException e) { @@ -278,12 +309,16 @@ public class PlayerDataFactory { if (boneIndices == null) return; - foreach (var kvp in boneIndices) + if (_logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value)); + foreach (var kvp in boneIndices) + { + _logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value)); + } } - if (boneIndices.All(u => u.Value.Count == 0)) return; + var maxPlayerBoneIndex = boneIndices.SelectMany(kvp => kvp.Value).DefaultIfEmpty().Max(); + if (maxPlayerBoneIndex <= 0) return; int noValidationFailed = 0; foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList()) @@ -303,12 +338,13 @@ public class PlayerDataFactory _logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count); - foreach (var boneCount in skeletonIndices.Select(k => k).ToList()) + foreach (var boneCount in skeletonIndices) { - if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max()) + var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max(); + if (maxAnimationIndex > maxPlayerBoneIndex) { _logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})", - file.ResolvedPath, boneCount.Key, boneCount.Value.Max(), boneIndices.SelectMany(b => b.Value).Max()); + file.ResolvedPath, boneCount.Key, maxAnimationIndex, maxPlayerBoneIndex); validationFailed = true; break; } diff --git a/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs b/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs index 8d56b4f..65709d1 100644 --- a/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs +++ b/LightlessSync/PlayerData/Handlers/GameObjectHandler.cs @@ -5,6 +5,7 @@ using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; using System.Runtime.CompilerServices; using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer; +using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind; namespace LightlessSync.PlayerData.Handlers; @@ -94,6 +95,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP public DrawCondition CurrentDrawCondition { get; set; } = DrawCondition.None; public byte Gender { get; private set; } public string Name { get; private set; } + public uint EntityId { get; private set; } = uint.MaxValue; public ObjectKind ObjectKind { get; } public byte RaceId { get; private set; } public byte TribeId { get; private set; } @@ -142,6 +144,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP { Address = IntPtr.Zero; DrawObjectAddress = IntPtr.Zero; + EntityId = uint.MaxValue; _haltProcessing = false; } @@ -171,13 +174,16 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP Address = _getAddress(); if (Address != IntPtr.Zero) { - var drawObjAddr = (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->DrawObject; + var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address; + var drawObjAddr = (IntPtr)gameObject->DrawObject; DrawObjectAddress = drawObjAddr; + EntityId = gameObject->EntityId; CurrentDrawCondition = DrawCondition.None; } else { DrawObjectAddress = IntPtr.Zero; + EntityId = uint.MaxValue; CurrentDrawCondition = DrawCondition.DrawObjectZero; } @@ -371,8 +377,8 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP { if (Address == IntPtr.Zero) return DrawCondition.ObjectZero; if (DrawObjectAddress == IntPtr.Zero) return DrawCondition.DrawObjectZero; - var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags) != 0x0; - if (renderFlags) return DrawCondition.RenderFlags; + var visibilityFlags = ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags; + if (visibilityFlags != VisibilityFlags.None) return DrawCondition.RenderFlags; if (ObjectKind == ObjectKind.Player) { diff --git a/LightlessSync/PlayerData/Handlers/PairHandler.cs b/LightlessSync/PlayerData/Handlers/PairHandler.cs deleted file mode 100644 index fb03bfb..0000000 --- a/LightlessSync/PlayerData/Handlers/PairHandler.cs +++ /dev/null @@ -1,775 +0,0 @@ -using LightlessSync.API.Data; -using LightlessSync.FileCache; -using LightlessSync.Interop.Ipc; -using LightlessSync.PlayerData.Factories; -using LightlessSync.PlayerData.Pairs; -using LightlessSync.Services; -using LightlessSync.Services.Events; -using LightlessSync.Services.Mediator; -using LightlessSync.Services.ServerConfiguration; -using LightlessSync.Utils; -using LightlessSync.WebAPI.Files; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System.Collections.Concurrent; -using System.Diagnostics; -using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind; - -namespace LightlessSync.PlayerData.Handlers; - -public sealed class PairHandler : DisposableMediatorSubscriberBase -{ - private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced); - - private readonly DalamudUtilService _dalamudUtil; - private readonly FileDownloadManager _downloadManager; - private readonly FileCacheManager _fileDbManager; - private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; - private readonly IpcManager _ipcManager; - private readonly IHostApplicationLifetime _lifetime; - private readonly PlayerPerformanceService _playerPerformanceService; - private readonly 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 _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 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(this, (_) => FrameworkUpdate()); - Mediator.Subscribe(this, (_) => - { - _downloadCancellationTokenSource?.CancelDispose(); - _charaHandler?.Invalidate(); - IsVisible = false; - }); - Mediator.Subscribe(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(this, (msg) => - { - if (msg.GameObjectHandler == _charaHandler) - { - _redrawOnNextApplication = true; - } - }); - Mediator.Subscribe(this, (msg) => - { - EnableSync(); - }); - Mediator.Subscribe(this, _ => - { - DisableSync(); - }); - Mediator.Subscribe(this, (msg) => - { - EnableSync(); - }); - Mediator.Subscribe(this, _ => - { - DisableSync(); - }); - Mediator.Subscribe(this, _ => - { - DisableSync(); - }); - Mediator.Subscribe(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> 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> 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> 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> 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 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> 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(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(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 TryCalculateModdedDictionary(Guid applicationBase, CharacterData charaData, out Dictionary<(string GamePath, string? Hash), string> moddedDictionary, CancellationToken token) - { - Stopwatch st = Stopwatch.StartNew(); - ConcurrentBag 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; - } - } -} diff --git a/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs new file mode 100644 index 0000000..5561bfe --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs @@ -0,0 +1,38 @@ + using LightlessSync.API.Data; + + namespace LightlessSync.PlayerData.Pairs; + + /// + /// orchestrates the lifecycle of a paired character + /// + 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 LastBlockingConditions { get; } + bool IsApplying { get; } + bool IsDownloading { get; } + int PendingDownloadCount { get; } + int ForbiddenDownloadCount { get; } + DateTime? InvisibleSinceUtc { get; } + DateTime? VisibilityEvictionDueAtUtc { get; } + + void Initialize(); + void ApplyData(CharacterData data); + void ApplyLastReceivedData(bool forced = false); + bool FetchPerformanceMetricsFromCache(); + void LoadCachedCharacterData(CharacterData data); + void SetUploading(bool uploading); + void SetPaused(bool paused); + } diff --git a/LightlessSync/PlayerData/Pairs/IPairHandlerAdapterFactory.cs b/LightlessSync/PlayerData/Pairs/IPairHandlerAdapterFactory.cs new file mode 100644 index 0000000..167b5bc --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/IPairHandlerAdapterFactory.cs @@ -0,0 +1,6 @@ +namespace LightlessSync.PlayerData.Pairs; + +public interface IPairHandlerAdapterFactory +{ + IPairHandlerAdapter Create(string ident); +} diff --git a/LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs b/LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs new file mode 100644 index 0000000..cd62f98 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/IPairPerformanceSubject.cs @@ -0,0 +1,19 @@ +using LightlessSync.API.Data; + +namespace LightlessSync.PlayerData.Pairs; + +/// +/// performance metrics for each pair handler +/// +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; } +} diff --git a/LightlessSync/PlayerData/Pairs/OptionalPluginWarning.cs b/LightlessSync/PlayerData/Pairs/OptionalPluginWarning.cs deleted file mode 100644 index a5c5eff..0000000 --- a/LightlessSync/PlayerData/Pairs/OptionalPluginWarning.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace LightlessSync.PlayerData.Pairs; - -public record OptionalPluginWarning -{ - public bool ShownHeelsWarning { get; set; } = false; - public bool ShownCustomizePlusWarning { get; set; } = false; - public bool ShownHonorificWarning { get; set; } = false; - public bool ShownMoodlesWarning { get; set; } = false; - public bool ShowPetNicknamesWarning { get; set; } = false; -} \ No newline at end of file diff --git a/LightlessSync/PlayerData/Pairs/Pair.cs b/LightlessSync/PlayerData/Pairs/Pair.cs index d4e2950..935b705 100644 --- a/LightlessSync/PlayerData/Pairs/Pair.cs +++ b/LightlessSync/PlayerData/Pairs/Pair.cs @@ -1,173 +1,156 @@ -using Dalamud.Game.Gui.ContextMenu; +using Dalamud.Game.Gui.ContextMenu; using Dalamud.Game.Text.SeStringHandling; using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.User; -using LightlessSync.PlayerData.Factories; -using LightlessSync.PlayerData.Handlers; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; -using LightlessSync.Utils; +using LightlessSync.UI; +using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; namespace LightlessSync.PlayerData.Pairs; +/// +/// ui wrapper around a pair connection +/// public class Pair { - private readonly PairHandlerFactory _cachedPlayerFactory; - private readonly SemaphoreSlim _creationSemaphore = new(1); + private readonly PairLedger _pairLedger; private readonly ILogger _logger; private readonly LightlessMediator _mediator; private readonly ServerConfigurationManager _serverConfigurationManager; - private CancellationTokenSource _applicationCts = new(); - private OnlineUserIdentDto? _onlineUserIdentDto = null; + private readonly Lazy _apiController; - public Pair(ILogger logger, UserFullPairDto userPair, PairHandlerFactory cachedPlayerFactory, - LightlessMediator mediator, ServerConfigurationManager serverConfigurationManager) + private const int _lightlessPrefixColor = 708; + + public Pair( + ILogger logger, + UserFullPairDto userPair, + PairLedger pairLedger, + LightlessMediator mediator, + ServerConfigurationManager serverConfigurationManager, + Lazy apiController) { _logger = logger; UserPair = userPair; - _cachedPlayerFactory = cachedPlayerFactory; + _pairLedger = pairLedger; _mediator = mediator; _serverConfigurationManager = serverConfigurationManager; + _apiController = apiController; } - public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName) && _onlineUserIdentDto != null; + private PairUniqueIdentifier PairIdent => UniqueIdent; + + private IPairHandlerAdapter? TryGetHandler() + { + return _pairLedger.GetHandler(PairIdent); + } + + private PairConnection? TryGetConnection() + { + return _pairLedger.TryGetEntry(PairIdent, out var entry) && entry is not null + ? entry.Connection + : null; + } + + public bool HasCachedPlayer => TryGetHandler() is not null; public IndividualPairStatus IndividualPairStatus => UserPair.IndividualPairStatus; public bool IsDirectlyPaired => IndividualPairStatus != IndividualPairStatus.None; public bool IsOneSidedPair => IndividualPairStatus == IndividualPairStatus.OneSided; - public bool IsOnline => CachedPlayer != null; + + public bool IsOnline => TryGetConnection()?.IsOnline ?? false; public bool IsPaired => IndividualPairStatus == IndividualPairStatus.Bidirectional || UserPair.Groups.Any(); public bool IsPaused => UserPair.OwnPermissions.IsPaused(); - public bool IsVisible => CachedPlayer?.IsVisible ?? false; - public CharacterData? LastReceivedCharacterData { get; set; } - public string? PlayerName => CachedPlayer?.PlayerName ?? string.Empty; - public long LastAppliedDataBytes => CachedPlayer?.LastAppliedDataBytes ?? -1; - public long LastAppliedDataTris { get; set; } = -1; - public long LastAppliedApproximateVRAMBytes { get; set; } = -1; - public string Ident => _onlineUserIdentDto?.Ident ?? string.Empty; - public uint PlayerCharacterId => CachedPlayer?.PlayerCharacterId ?? uint.MaxValue; + public bool IsVisible => _pairLedger.IsPairVisible(PairIdent); + public CharacterData? LastReceivedCharacterData => TryGetHandler()?.LastReceivedCharacterData; + public string? PlayerName => TryGetHandler()?.PlayerName ?? UserPair.User.AliasOrUID; + public long LastAppliedDataBytes => TryGetHandler()?.LastAppliedDataBytes ?? -1; + public long LastAppliedDataTris => TryGetHandler()?.LastAppliedDataTris ?? -1; + public long LastAppliedApproximateVRAMBytes => TryGetHandler()?.LastAppliedApproximateVRAMBytes ?? -1; + public long LastAppliedApproximateEffectiveVRAMBytes => TryGetHandler()?.LastAppliedApproximateEffectiveVRAMBytes ?? -1; + public string Ident => TryGetHandler()?.Ident ?? TryGetConnection()?.Ident ?? string.Empty; + public uint PlayerCharacterId => TryGetHandler()?.PlayerCharacterId ?? uint.MaxValue; + public PairUniqueIdentifier UniqueIdent => new(UserData.UID); public UserData UserData => UserPair.User; public UserFullPairDto UserPair { get; set; } - private PairHandler? CachedPlayer { get; set; } public void AddContextMenu(IMenuOpenedArgs args) { - if (CachedPlayer == null || (args.Target is not MenuTargetDefault target) || target.TargetObjectId != CachedPlayer.PlayerCharacterId || IsPaused) return; - - SeStringBuilder seStringBuilder = new(); - SeStringBuilder seStringBuilder2 = new(); - SeStringBuilder seStringBuilder3 = new(); - SeStringBuilder seStringBuilder4 = new(); - var openProfileSeString = seStringBuilder.AddText("Open Profile").Build(); - var reapplyDataSeString = seStringBuilder2.AddText("Reapply last data").Build(); - var cyclePauseState = seStringBuilder3.AddText("Cycle pause state").Build(); - var changePermissions = seStringBuilder4.AddText("Change Permissions").Build(); - args.AddMenuItem(new MenuItem() + var handler = TryGetHandler(); + if (handler is null) { - Name = openProfileSeString, - OnClicked = (a) => _mediator.Publish(new ProfileOpenStandaloneMessage(this)), - UseDefaultPrefix = false, - PrefixChar = 'L', - PrefixColor = 708 + return; + } + + if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId || IsPaused) + { + return; + } + + UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () => + { + _mediator.Publish(new ProfileOpenStandaloneMessage(this)); + return Task.CompletedTask; }); - args.AddMenuItem(new MenuItem() + UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () => { - Name = reapplyDataSeString, - OnClicked = (a) => ApplyLastReceivedData(forced: true), - UseDefaultPrefix = false, - PrefixChar = 'L', - PrefixColor = 708 + ApplyLastReceivedData(forced: true); + return Task.CompletedTask; }); - args.AddMenuItem(new MenuItem() + UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () => { - Name = changePermissions, - OnClicked = (a) => _mediator.Publish(new OpenPermissionWindow(this)), - UseDefaultPrefix = false, - PrefixChar = 'L', - PrefixColor = 708 + _mediator.Publish(new OpenPermissionWindow(this)); + return Task.CompletedTask; }); - args.AddMenuItem(new MenuItem() + UiSharedService.AddContextMenuItem(args, name: "Cycle pause state", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () => { - Name = cyclePauseState, - OnClicked = (a) => _mediator.Publish(new CyclePauseMessage(UserData)), - UseDefaultPrefix = false, - PrefixChar = 'L', - PrefixColor = 708 + TriggerCyclePause(); + return Task.CompletedTask; }); } public void ApplyData(OnlineUserCharaDataDto data) { - _applicationCts = _applicationCts.CancelRecreate(); - LastReceivedCharacterData = data.CharaData; + _logger.LogTrace("Character data received for {Uid}; handler will process via registry.", UserData.UID); + } - if (CachedPlayer == null) - { - _logger.LogDebug("Received Data for {uid} but CachedPlayer does not exist, waiting", data.User.UID); - _ = Task.Run(async () => - { - using var timeoutCts = new CancellationTokenSource(); - timeoutCts.CancelAfter(TimeSpan.FromSeconds(120)); - var appToken = _applicationCts.Token; - using var combined = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, appToken); - while (CachedPlayer == null && !combined.Token.IsCancellationRequested) - { - await Task.Delay(250, combined.Token).ConfigureAwait(false); - } - - if (!combined.IsCancellationRequested) - { - _logger.LogDebug("Applying delayed data for {uid}", data.User.UID); - ApplyLastReceivedData(); - } - }); - return; - } - - ApplyLastReceivedData(); + private void TriggerCyclePause() + { + _ = _apiController.Value.CyclePauseAsync(this); } public void ApplyLastReceivedData(bool forced = false) { - if (CachedPlayer == null) return; - if (LastReceivedCharacterData == null) return; + var handler = TryGetHandler(); + if (handler is null) + { + _logger.LogTrace("ApplyLastReceivedData skipped for {Uid}: handler missing.", UserData.UID); + return; + } - CachedPlayer.ApplyCharacterData(Guid.NewGuid(), RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, forced); + handler.ApplyLastReceivedData(forced); } public void CreateCachedPlayer(OnlineUserIdentDto? dto = null) { - try + var handler = TryGetHandler(); + if (handler is null) { - _creationSemaphore.Wait(); - - if (CachedPlayer != null) return; - - if (dto == null && _onlineUserIdentDto == null) - { - CachedPlayer?.Dispose(); - CachedPlayer = null; - return; - } - if (dto != null) - { - _onlineUserIdentDto = dto; - } - - CachedPlayer?.Dispose(); - CachedPlayer = _cachedPlayerFactory.Create(this); + _logger.LogTrace("CreateCachedPlayer skipped for {Uid}: handler unavailable.", UserData.UID); + return; } - finally + + if (!handler.Initialized) { - _creationSemaphore.Release(); + handler.Initialize(); } } @@ -178,7 +161,7 @@ public class Pair public string GetPlayerNameHash() { - return CachedPlayer?.PlayerNameHash ?? string.Empty; + return TryGetHandler()?.PlayerNameHash ?? string.Empty; } public bool HasAnyConnection() @@ -188,21 +171,7 @@ public class Pair public void MarkOffline(bool wait = true) { - try - { - if (wait) - _creationSemaphore.Wait(); - LastReceivedCharacterData = null; - var player = CachedPlayer; - CachedPlayer = null; - player?.Dispose(); - _onlineUserIdentDto = null; - } - finally - { - if (wait) - _creationSemaphore.Release(); - } + _logger.LogTrace("MarkOffline invoked for {Uid} (wait: {Wait}). New registry handles handler disposal.", UserData.UID, wait); } public void SetNote(string note) @@ -212,47 +181,43 @@ public class Pair internal void SetIsUploading() { - CachedPlayer?.SetUploading(); + var handler = TryGetHandler(); + if (handler is null) + { + return; + } + + handler.SetUploading(true); } - private CharacterData? RemoveNotSyncedFiles(CharacterData? data) + public PairDebugInfo GetDebugInfo() { - _logger.LogTrace("Removing not synced files"); - if (data == null) - { - _logger.LogTrace("Nothing to remove"); - return data; - } + var handler = TryGetHandler(); + if (handler is null) + return PairDebugInfo.Empty; - bool disableIndividualAnimations = (UserPair.OtherPermissions.IsDisableAnimations() || UserPair.OwnPermissions.IsDisableAnimations()); - bool disableIndividualVFX = (UserPair.OtherPermissions.IsDisableVFX() || UserPair.OwnPermissions.IsDisableVFX()); - bool disableIndividualSounds = (UserPair.OtherPermissions.IsDisableSounds() || UserPair.OwnPermissions.IsDisableSounds()); + var now = DateTime.UtcNow; + var dueAt = handler.VisibilityEvictionDueAtUtc; + var remainingSeconds = dueAt.HasValue + ? Math.Max(0, (dueAt.Value - now).TotalSeconds) + : (double?)null; - _logger.LogTrace("Disable: Sounds: {disableIndividualSounds}, Anims: {disableIndividualAnims}; " + - "VFX: {disableGroupSounds}", - disableIndividualSounds, disableIndividualAnimations, disableIndividualVFX); - - if (disableIndividualAnimations || disableIndividualSounds || disableIndividualVFX) - { - _logger.LogTrace("Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}, VFX disabled: {disableVFX}", - disableIndividualAnimations, disableIndividualSounds, disableIndividualVFX); - foreach (var objectKind in data.FileReplacements.Select(k => k.Key)) - { - if (disableIndividualSounds) - data.FileReplacements[objectKind] = data.FileReplacements[objectKind] - .Where(f => !f.GamePaths.Any(p => p.EndsWith("scd", StringComparison.OrdinalIgnoreCase))) - .ToList(); - if (disableIndividualAnimations) - data.FileReplacements[objectKind] = data.FileReplacements[objectKind] - .Where(f => !f.GamePaths.Any(p => p.EndsWith("tmb", StringComparison.OrdinalIgnoreCase) || p.EndsWith("pap", StringComparison.OrdinalIgnoreCase))) - .ToList(); - if (disableIndividualVFX) - data.FileReplacements[objectKind] = data.FileReplacements[objectKind] - .Where(f => !f.GamePaths.Any(p => p.EndsWith("atex", StringComparison.OrdinalIgnoreCase) || p.EndsWith("avfx", StringComparison.OrdinalIgnoreCase))) - .ToList(); - } - } - - return data; + return new PairDebugInfo( + true, + handler.Initialized, + handler.IsVisible, + handler.ScheduledForDeletion, + handler.LastDataReceivedAt, + handler.LastApplyAttemptAt, + handler.LastSuccessfulApplyAt, + handler.InvisibleSinceUtc, + handler.VisibilityEvictionDueAtUtc, + remainingSeconds, + handler.LastFailureReason, + handler.LastBlockingConditions, + handler.IsApplying, + handler.IsDownloading, + handler.PendingDownloadCount, + handler.ForbiddenDownloadCount); } -} \ No newline at end of file +} diff --git a/LightlessSync/PlayerData/Pairs/PairCoordinator.Groups.cs b/LightlessSync/PlayerData/Pairs/PairCoordinator.Groups.cs new file mode 100644 index 0000000..7bdfc23 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairCoordinator.Groups.cs @@ -0,0 +1,136 @@ +using LightlessSync.API.Dto.Group; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.PlayerData.Pairs; + +/// +/// handles group related pair events +/// +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); + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairCoordinator.Users.cs b/LightlessSync/PlayerData/Pairs/PairCoordinator.Users.cs new file mode 100644 index 0000000..0891035 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairCoordinator.Users.cs @@ -0,0 +1,302 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto.User; +using LightlessSync.Services.Events; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.PlayerData.Pairs; + +/// +/// handles user pair events +/// +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)); + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairCoordinator.cs b/LightlessSync/PlayerData/Pairs/PairCoordinator.cs new file mode 100644 index 0000000..3333eaa --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairCoordinator.cs @@ -0,0 +1,139 @@ +using System.Collections.Concurrent; +using LightlessSync.API.Dto.User; +using LightlessSync.LightlessConfiguration; +using LightlessSync.LightlessConfiguration.Models; +using LightlessSync.Services.Mediator; +using LightlessSync.Services.ServerConfiguration; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.PlayerData.Pairs; + +/// +/// wires mediator events into the pair system +/// +public sealed partial class PairCoordinator : MediatorSubscriberBase +{ + private readonly ILogger _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 _pendingCharacterData = new(StringComparer.Ordinal); + + public PairCoordinator( + ILogger 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(this, msg => HandleActiveServerChange(msg.ServerUrl)); + mediator.Subscribe(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); + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairDebugInfo.cs b/LightlessSync/PlayerData/Pairs/PairDebugInfo.cs new file mode 100644 index 0000000..31c3236 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairDebugInfo.cs @@ -0,0 +1,38 @@ +namespace LightlessSync.PlayerData.Pairs; + +public sealed record PairDebugInfo( + bool HasHandler, + bool HandlerInitialized, + bool HandlerVisible, + bool HandlerScheduledForDeletion, + DateTime? LastDataReceivedAt, + DateTime? LastApplyAttemptAt, + DateTime? LastSuccessfulApplyAt, + DateTime? InvisibleSinceUtc, + DateTime? VisibilityEvictionDueAtUtc, + double? VisibilityEvictionRemainingSeconds, + string? LastFailureReason, + IReadOnlyList BlockingConditions, + bool IsApplying, + bool IsDownloading, + int PendingDownloadCount, + int ForbiddenDownloadCount) +{ + public static PairDebugInfo Empty { get; } = new( + false, + false, + false, + false, + null, + null, + null, + null, + null, + null, + null, + Array.Empty(), + false, + false, + 0, + 0); +} diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs new file mode 100644 index 0000000..706b0bc --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -0,0 +1,1983 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Data.Extensions; +using LightlessSync.FileCache; +using LightlessSync.Interop.Ipc; +using LightlessSync.PlayerData.Factories; +using LightlessSync.PlayerData.Handlers; +using LightlessSync.Services; +using LightlessSync.Services.Events; +using LightlessSync.Services.Mediator; +using LightlessSync.Services.PairProcessing; +using LightlessSync.Services.ServerConfiguration; +using LightlessSync.Services.TextureCompression; +using LightlessSync.Utils; +using LightlessSync.WebAPI.Files; +using LightlessSync.WebAPI.Files.Models; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind; +using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer; + +namespace LightlessSync.PlayerData.Pairs; + +/// +/// handles lifecycle, visibility, queued data, character data for a paired user +/// +internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPairHandlerAdapter +{ + 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 readonly TextureDownscaleService _textureDownscaleService; + private readonly PairStateCache _pairStateCache; + private readonly PairPerformanceMetricsCache _performanceMetricsCache; + private readonly PairManager _pairManager; + private CancellationTokenSource? _applicationCancellationTokenSource; + private Guid _applicationId; + private Task? _applicationTask; + private CharacterData? _cachedData = null; + private GameObjectHandler? _charaHandler; + private readonly Dictionary _customizeIds = []; + private CombatData? _dataReceivedInDowntime; + private CancellationTokenSource? _downloadCancellationTokenSource; + private bool _forceApplyMods = false; + private bool _forceFullReapply; + private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths; + private bool _needsCollectionRebuild; + private bool _isVisible; + private Guid _penumbraCollection; + private readonly object _collectionGate = new(); + private bool _redrawOnNextApplication = false; + private bool _explicitRedrawQueued; + private readonly object _initializationGate = new(); + private readonly object _pauseLock = new(); + private Task _pauseTransitionTask = Task.CompletedTask; + private bool _pauseRequested; + private DateTime? _lastDataReceivedAt; + private DateTime? _lastApplyAttemptAt; + private DateTime? _lastSuccessfulApplyAt; + private string? _lastFailureReason; + private IReadOnlyList _lastBlockingConditions = Array.Empty(); + private readonly object _visibilityGraceGate = new(); + private CancellationTokenSource? _visibilityGraceCts; + private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1); + private DateTime? _invisibleSinceUtc; + private DateTime? _visibilityEvictionDueAtUtc; + + public DateTime? InvisibleSinceUtc => _invisibleSinceUtc; + public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc; + public string Ident { get; } + public bool Initialized { get; private set; } + public bool ScheduledForDeletion { get; set; } + + public bool IsVisible + { + get => _isVisible; + private set + { + if (_isVisible == value) return; + + _isVisible = value; + + if (!_isVisible) + { + DisableSync(); + + _invisibleSinceUtc = DateTime.UtcNow; + _visibilityEvictionDueAtUtc = _invisibleSinceUtc.Value.Add(VisibilityEvictionGrace); + + StartVisibilityGraceTask(); + } + else + { + CancelVisibilityGraceTask(); + + _invisibleSinceUtc = null; + _visibilityEvictionDueAtUtc = null; + + ScheduledForDeletion = false; + + if (_charaHandler is not null && _charaHandler.Address != nint.Zero) + _ = EnsurePenumbraCollection(); + } + + var user = GetPrimaryUserData(); + Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), + EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible")))); + Mediator.Publish(new RefreshUiMessage()); + Mediator.Publish(new VisibilityChange()); + } + } + + public long LastAppliedDataBytes { get; private set; } + public long LastAppliedDataTris { get; set; } = -1; + public long LastAppliedApproximateVRAMBytes { get; set; } = -1; + public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1; + public CharacterData? LastReceivedCharacterData { get; private set; } + public DateTime? LastDataReceivedAt => _lastDataReceivedAt; + public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt; + public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt; + public string? LastFailureReason => _lastFailureReason; + public IReadOnlyList LastBlockingConditions => _lastBlockingConditions; + public bool IsApplying => _applicationTask is { IsCompleted: false }; + public bool IsDownloading => _downloadManager.IsDownloading; + public int PendingDownloadCount => _downloadManager.CurrentDownloads.Count; + public int ForbiddenDownloadCount => _downloadManager.ForbiddenTransfers.Count; + + public PairHandlerAdapter( + ILogger logger, + LightlessMediator mediator, + PairManager pairManager, + string ident, + GameObjectHandlerFactory gameObjectHandlerFactory, + IpcManager ipcManager, + FileDownloadManager transferManager, + PluginWarningNotificationService pluginWarningNotificationManager, + DalamudUtilService dalamudUtil, + IHostApplicationLifetime lifetime, + FileCacheManager fileDbManager, + PlayerPerformanceService playerPerformanceService, + PairProcessingLimiter pairProcessingLimiter, + ServerConfigurationManager serverConfigManager, + TextureDownscaleService textureDownscaleService, + PairStateCache pairStateCache, + PairPerformanceMetricsCache performanceMetricsCache) : base(logger, mediator) + { + _pairManager = pairManager; + Ident = ident; + _gameObjectHandlerFactory = gameObjectHandlerFactory; + _ipcManager = ipcManager; + _downloadManager = transferManager; + _pluginWarningNotificationManager = pluginWarningNotificationManager; + _dalamudUtil = dalamudUtil; + _lifetime = lifetime; + _fileDbManager = fileDbManager; + _playerPerformanceService = playerPerformanceService; + _pairProcessingLimiter = pairProcessingLimiter; + _serverConfigManager = serverConfigManager; + _textureDownscaleService = textureDownscaleService; + _pairStateCache = pairStateCache; + _performanceMetricsCache = performanceMetricsCache; + LastAppliedDataBytes = -1; + } + + public void Initialize() + { + EnsureInitialized(); + } + + private void EnsureInitialized() + { + if (Initialized) + { + return; + } + + lock (_initializationGate) + { + if (Initialized) + { + return; + } + + if (LastAppliedDataBytes < 0 || LastAppliedDataTris < 0 + || LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) + { + _forceApplyMods = true; + } + + Mediator.Subscribe(this, _ => FrameworkUpdate()); + Mediator.Subscribe(this, _ => + { + _downloadCancellationTokenSource?.CancelDispose(); + _charaHandler?.Invalidate(); + IsVisible = false; + }); + Mediator.Subscribe(this, _ => + { + ResetPenumbraCollection(releaseFromPenumbra: false, reason: "PenumbraInitialized"); + if (!IsVisible && _charaHandler is not null) + { + PlayerName = string.Empty; + _charaHandler.Dispose(); + _charaHandler = null; + } + EnableSync(); + }); + Mediator.Subscribe(this, _ => ResetPenumbraCollection(releaseFromPenumbra: false, reason: "PenumbraDisposed")); + Mediator.Subscribe(this, msg => + { + if (msg.GameObjectHandler == _charaHandler) + { + _redrawOnNextApplication = true; + } + }); + Mediator.Subscribe(this, _ => EnableSync()); + Mediator.Subscribe(this, _ => DisableSync()); + Mediator.Subscribe(this, _ => EnableSync()); + Mediator.Subscribe(this, _ => DisableSync()); + Mediator.Subscribe(this, _ => DisableSync()); + Mediator.Subscribe(this, _ => EnableSync()); + Mediator.Subscribe(this, _ => DisableSync()); + Mediator.Subscribe(this, _ => EnableSync()); + Mediator.Subscribe(this, _ => DisableSync()); + Mediator.Subscribe(this, _ => EnableSync()); + Mediator.Subscribe(this, msg => + { + if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler)) + { + return; + } + TryApplyQueuedData(); + }); + + Initialized = true; + } + } + + private IReadOnlyList GetCurrentPairs() + { + return _pairManager.GetPairsByIdent(Ident); + } + + private PairConnection? GetPrimaryPair() + { + var pairs = GetCurrentPairs(); + var direct = pairs.FirstOrDefault(p => p.IsDirectlyPaired); + if (direct is not null) + { + return direct; + } + + var online = pairs.FirstOrDefault(p => p.IsOnline); + if (online is not null) + { + return online; + } + + return pairs.FirstOrDefault(); + } + + private UserData GetPrimaryUserData() + { + return GetPrimaryPair()?.User ?? new UserData(Ident); + } + + private string GetPrimaryAliasOrUid() + { + var pair = GetPrimaryPair(); + if (pair?.User is null) + { + return Ident; + } + + return string.IsNullOrEmpty(pair.User.AliasOrUID) ? Ident : pair.User.AliasOrUID; + } + + private string GetPrimaryAliasOrUidSafe() + { + try + { + return GetPrimaryAliasOrUid(); + } + catch + { + return Ident; + } + } + + private UserData GetPrimaryUserDataSafe() + { + try + { + return GetPrimaryUserData(); + } + catch + { + return new UserData(Ident); + } + } + + private string GetLogIdentifier() + { + var alias = GetPrimaryAliasOrUidSafe(); + return string.Equals(alias, Ident, StringComparison.Ordinal) ? alias : $"{alias} ({Ident})"; + } + + private Guid EnsurePenumbraCollection() + { + if (!IsVisible) + { + return Guid.Empty; + } + + if (_penumbraCollection != Guid.Empty) + { + return _penumbraCollection; + } + + lock (_collectionGate) + { + if (_penumbraCollection != Guid.Empty) + { + return _penumbraCollection; + } + + var cached = _pairStateCache.TryGetTemporaryCollection(Ident); + if (cached.HasValue && cached.Value != Guid.Empty) + { + _penumbraCollection = cached.Value; + return _penumbraCollection; + } + + if (!_ipcManager.Penumbra.APIAvailable) + { + return Guid.Empty; + } + + var user = GetPrimaryUserDataSafe(); + var uid = !string.IsNullOrEmpty(user.UID) ? user.UID : Ident; + var created = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(Logger, uid) + .ConfigureAwait(false).GetAwaiter().GetResult(); + if (created != Guid.Empty) + { + _penumbraCollection = created; + _pairStateCache.StoreTemporaryCollection(Ident, created); + } + + return _penumbraCollection; + } + } + + private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null) + { + Guid toRelease = Guid.Empty; + bool hadCollection = false; + lock (_collectionGate) + { + if (_penumbraCollection != Guid.Empty) + { + toRelease = _penumbraCollection; + _penumbraCollection = Guid.Empty; + hadCollection = true; + } + } + + var cached = _pairStateCache.ClearTemporaryCollection(Ident); + if (cached.HasValue && cached.Value != Guid.Empty) + { + toRelease = cached.Value; + hadCollection = true; + } + + if (hadCollection) + { + _needsCollectionRebuild = true; + _forceFullReapply = true; + _forceApplyMods = true; + } + + if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable) + { + return; + } + + try + { + var applicationId = Guid.NewGuid(); + Logger.LogTrace("[{applicationId}] Removing temp collection {CollectionId} for {handler} ({reason})", applicationId, toRelease, GetLogIdentifier(), reason ?? "Cleanup"); + _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, toRelease).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to remove temporary Penumbra collection for {handler}", GetLogIdentifier()); + } + } + + private bool AnyPair(Func predicate) + { + return GetCurrentPairs().Any(predicate); + } + + private bool ShouldSkipDownscale() + { + return GetCurrentPairs().Any(p => p.IsDirectlyPaired && p.SelfToOtherPermissions.IsSticky()); + } + + private bool IsPaused() + { + var pairs = GetCurrentPairs(); + return pairs.Count > 0 && pairs.Any(p => p.IsPaused); + } + + bool IPairPerformanceSubject.IsPaused => IsPaused(); + + bool IPairPerformanceSubject.IsDirectlyPaired => AnyPair(p => p.IsDirectlyPaired); + + bool IPairPerformanceSubject.HasStickyPermissions => AnyPair(p => p.SelfToOtherPermissions.HasFlag(UserPermissions.Sticky)); + + UserData IPairPerformanceSubject.UserData => GetPrimaryUserData(); + + string IPairPerformanceSubject.PlayerName => PlayerName ?? GetPrimaryAliasOrUidSafe(); + private UserPermissions GetCombinedPermissions() + { + var pairs = GetCurrentPairs(); + if (pairs.Count == 0) + { + return UserPermissions.NoneSet; + } + + var combined = pairs[0].SelfToOtherPermissions | pairs[0].OtherToSelfPermissions; + for (int i = 1; i < pairs.Count; i++) + { + var perms = pairs[i].SelfToOtherPermissions | pairs[i].OtherToSelfPermissions; + combined &= perms; + } + + return combined; + } + public nint PlayerCharacter => _charaHandler?.Address ?? nint.Zero; + public uint PlayerCharacterId => _charaHandler?.EntityId ?? uint.MaxValue; + public string? PlayerName { get; private set; } + public string PlayerNameHash => Ident; + + public void ApplyData(CharacterData data) + { + EnsureInitialized(); + LastReceivedCharacterData = data; + _lastDataReceivedAt = DateTime.UtcNow; + ApplyLastReceivedData(); + } + + public void LoadCachedCharacterData(CharacterData data) + { + if (data is null) + { + return; + } + + LastReceivedCharacterData = data; + _cachedData = null; + _forceApplyMods = true; + LastAppliedDataBytes = -1; + LastAppliedDataTris = -1; + LastAppliedApproximateVRAMBytes = -1; + LastAppliedApproximateEffectiveVRAMBytes = -1; + } + + public void ApplyLastReceivedData(bool forced = false) + { + EnsureInitialized(); + if (LastReceivedCharacterData is null) + { + Logger.LogTrace("No cached data to apply for {Ident}", Ident); + return; + } + + var shouldForce = forced || HasMissingCachedFiles(LastReceivedCharacterData); + + if (IsPaused()) + { + Logger.LogTrace("Permissions paused for {Ident}, skipping reapply", Ident); + return; + } + + if (shouldForce) + { + _forceApplyMods = true; + _forceFullReapply = true; + LastAppliedDataBytes = -1; + LastAppliedDataTris = -1; + LastAppliedApproximateVRAMBytes = -1; + LastAppliedApproximateEffectiveVRAMBytes = -1; + } + + var sanitized = CloneAndSanitizeLastReceived(out _); + if (sanitized is null) + { + Logger.LogTrace("Sanitized data null for {Ident}", Ident); + return; + } + + _pairStateCache.Store(Ident, sanitized); + + if (!IsVisible) + { + Logger.LogTrace("Handler for {Ident} not visible, caching sanitized data for later", Ident); + _cachedData = sanitized; + _forceFullReapply = true; + return; + } + + ApplyCharacterData(Guid.NewGuid(), sanitized, shouldForce); + } + + public bool FetchPerformanceMetricsFromCache() + { + EnsureInitialized(); + var sanitized = CloneAndSanitizeLastReceived(out var dataHash); + if (sanitized is null || string.IsNullOrEmpty(dataHash)) + { + return false; + } + + if (!TryApplyCachedMetrics(dataHash)) + { + return false; + } + + _cachedData = sanitized; + _pairStateCache.Store(Ident, sanitized); + return true; + } + + private CharacterData? CloneAndSanitizeLastReceived(out string? dataHash) + { + dataHash = null; + if (LastReceivedCharacterData is null) + { + return null; + } + + var sanitized = RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone()); + if (sanitized is null) + { + return null; + } + + dataHash = GetDataHashSafe(sanitized); + return sanitized; + } + + private string? GetDataHashSafe(CharacterData data) + { + try + { + return data.DataHash.Value; + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to compute character data hash for {Ident}", Ident); + return null; + } + } + + private bool TryApplyCachedMetrics(string? dataHash) + { + if (string.IsNullOrEmpty(dataHash)) + { + return false; + } + + if (!_performanceMetricsCache.TryGetMetrics(Ident, dataHash, out var metrics)) + { + return false; + } + + ApplyCachedMetrics(metrics); + return true; + } + + private void ApplyCachedMetrics(PairPerformanceMetrics metrics) + { + LastAppliedDataTris = metrics.TriangleCount; + LastAppliedApproximateVRAMBytes = metrics.ApproximateVramBytes; + LastAppliedApproximateEffectiveVRAMBytes = metrics.ApproximateEffectiveVramBytes; + } + + private void StorePerformanceMetrics(CharacterData charaData) + { + if (LastAppliedDataTris < 0 + || LastAppliedApproximateVRAMBytes < 0 + || LastAppliedApproximateEffectiveVRAMBytes < 0) + { + return; + } + + var dataHash = GetDataHashSafe(charaData); + if (string.IsNullOrEmpty(dataHash)) + { + return; + } + + _performanceMetricsCache.StoreMetrics( + Ident, + dataHash, + new PairPerformanceMetrics(LastAppliedDataTris, LastAppliedApproximateVRAMBytes, LastAppliedApproximateEffectiveVRAMBytes)); + } + + private bool HasMissingCachedFiles(CharacterData characterData) + { + try + { + HashSet inspectedHashes = new(StringComparer.OrdinalIgnoreCase); + foreach (var replacements in characterData.FileReplacements.Values) + { + foreach (var replacement in replacements) + { + if (!string.IsNullOrEmpty(replacement.FileSwapPath)) + { + if (!File.Exists(replacement.FileSwapPath)) + { + Logger.LogTrace("Missing file swap path {Path} detected for {Handler}", replacement.FileSwapPath, GetLogIdentifier()); + return true; + } + continue; + } + + if (string.IsNullOrEmpty(replacement.Hash) || !inspectedHashes.Add(replacement.Hash)) + { + continue; + } + + var cacheEntry = _fileDbManager.GetFileCacheByHash(replacement.Hash); + if (cacheEntry is null) + { + Logger.LogTrace("Missing cached file {Hash} detected for {Handler}", replacement.Hash, GetLogIdentifier()); + return true; + } + + if (!File.Exists(cacheEntry.ResolvedFilepath)) + { + Logger.LogTrace("Cached file {Hash} missing on disk for {Handler}, removing cache entry", replacement.Hash, GetLogIdentifier()); + _fileDbManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath); + return true; + } + } + } + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to determine cache availability for {Handler}", GetLogIdentifier()); + } + + return false; + } + + private CharacterData? RemoveNotSyncedFiles(CharacterData? data) + { + Logger.LogTrace("Removing not synced files for {Ident}", Ident); + if (data is null) + { + return null; + } + + var permissions = GetCombinedPermissions(); + bool disableAnimations = permissions.IsDisableAnimations(); + bool disableVfx = permissions.IsDisableVFX(); + bool disableSounds = permissions.IsDisableSounds(); + + if (!(disableAnimations || disableVfx || disableSounds)) + { + return data; + } + + foreach (var objectKind in data.FileReplacements.Keys.ToList()) + { + var replacements = data.FileReplacements[objectKind]; + if (disableSounds) + { + replacements = replacements + .Where(f => !f.GamePaths.Any(p => p.EndsWith("scd", StringComparison.OrdinalIgnoreCase))) + .ToList(); + } + + if (disableAnimations) + { + replacements = replacements + .Where(f => !f.GamePaths.Any(p => + p.EndsWith("tmb", StringComparison.OrdinalIgnoreCase) || + p.EndsWith("pap", StringComparison.OrdinalIgnoreCase))) + .ToList(); + } + + if (disableVfx) + { + replacements = replacements + .Where(f => !f.GamePaths.Any(p => + p.EndsWith("atex", StringComparison.OrdinalIgnoreCase) || + p.EndsWith("avfx", StringComparison.OrdinalIgnoreCase))) + .ToList(); + } + + data.FileReplacements[objectKind] = replacements; + } + + return data; + } + + private bool HasValidCachedModdedPaths() + { + if (_lastAppliedModdedPaths is null || _lastAppliedModdedPaths.Count == 0) + { + return false; + } + + foreach (var entry in _lastAppliedModdedPaths) + { + if (string.IsNullOrEmpty(entry.Value) || !File.Exists(entry.Value)) + { + Logger.LogDebug("Cached file path {path} missing for {handler}, forcing recalculation", entry.Value ?? "empty", GetLogIdentifier()); + return false; + } + } + + return true; + } + + private bool CanApplyNow() + { + return !_dalamudUtil.IsInCombat + && !_dalamudUtil.IsPerforming + && !_dalamudUtil.IsInInstance + && !_dalamudUtil.IsInCutscene + && !_dalamudUtil.IsInGpose + && _ipcManager.Penumbra.APIAvailable + && _ipcManager.Glamourer.APIAvailable; + } + + private void RecordFailure(string reason, params string[] conditions) + { + _lastFailureReason = reason; + _lastBlockingConditions = conditions.Length == 0 ? Array.Empty() : conditions.ToArray(); + } + + private void ClearFailureState() + { + _lastFailureReason = null; + _lastBlockingConditions = Array.Empty(); + } + + public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false) + { + _lastApplyAttemptAt = DateTime.UtcNow; + ClearFailureState(); + + if (characterData is null) + { + RecordFailure("Received null character data", "InvalidData"); + Logger.LogWarning("[BASE-{appBase}] Received null character data, skipping application for {handler}", applicationBase, GetLogIdentifier()); + SetUploading(false); + return; + } + + var user = GetPrimaryUserData(); + if (_dalamudUtil.IsInCombat) + { + const string reason = "Cannot apply character data: you are in combat, deferring application"; + Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, + reason))); + Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase); + RecordFailure(reason, "Combat"); + _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); + SetUploading(false); + return; + } + + if (_dalamudUtil.IsPerforming) + { + const string reason = "Cannot apply character data: you are performing music, deferring application"; + Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, + reason))); + Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase); + RecordFailure(reason, "Performance"); + _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); + SetUploading(false); + return; + } + + if (_dalamudUtil.IsInInstance) + { + const string reason = "Cannot apply character data: you are in an instance, deferring application"; + Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, + reason))); + Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase); + RecordFailure(reason, "Instance"); + _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); + SetUploading(false); + return; + } + + if (_dalamudUtil.IsInCutscene) + { + const string reason = "Cannot apply character data: you are in a cutscene, deferring application"; + Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, + reason))); + Logger.LogDebug("[BASE-{appBase}] Received data but player is in a cutscene", applicationBase); + RecordFailure(reason, "Cutscene"); + _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); + SetUploading(false); + return; + } + + if (_dalamudUtil.IsInGpose) + { + const string reason = "Cannot apply character data: you are in GPose, deferring application"; + Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, + reason))); + Logger.LogDebug("[BASE-{appBase}] Received data but player is in GPose", applicationBase); + RecordFailure(reason, "GPose"); + _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); + SetUploading(false); + return; + } + + if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable) + { + const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application"; + Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, + reason))); + Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier()); + RecordFailure(reason, "PluginUnavailable"); + _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); + SetUploading(false); + return; + } + + var handlerReady = _charaHandler is not null && PlayerCharacter != IntPtr.Zero; + + if (!handlerReady) + { + Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), 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 || _cachedData == null; + _cachedData = characterData; + _forceFullReapply = true; + Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods); + } + + SetUploading(false); + + Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, GetLogIdentifier(), 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 (handlerReady + && string.Equals(characterData.DataHash.Value, _cachedData?.DataHash.Value ?? string.Empty, StringComparison.Ordinal) + && !forceApplyCustomization && !_forceApplyMods) + { + return; + } + + Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational, + "Applying Character Data"))); + + var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods); + + if (handlerReady && _forceApplyMods) + { + _forceApplyMods = false; + } + + _explicitRedrawQueued = false; + + if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player)) + { + player.Add(PlayerChanges.ForcedRedraw); + _redrawOnNextApplication = false; + _explicitRedrawQueued = true; + } + + if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges)) + { + _pluginWarningNotificationManager.NotifyForMissingPlugins(user, PlayerName!, playerChanges); + } + + Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, GetPrimaryAliasOrUidSafe()); + + var forceFullReapply = _forceFullReapply + || LastAppliedApproximateVRAMBytes < 0 || LastAppliedDataTris < 0; + + DownloadAndApplyCharacter(applicationBase, characterData.DeepClone(), charaDataToUpdate, forceFullReapply); + } + + public override string ToString() + { + var alias = GetPrimaryAliasOrUidSafe(); + return $"{alias}:{PlayerName ?? string.Empty}:{(PlayerCharacter != nint.Zero ? "HasChar" : "NoChar")}"; + } + + public void SetUploading(bool uploading) + { + Logger.LogTrace("Setting {name} uploading {uploading}", GetPrimaryAliasOrUidSafe(), uploading); + if (_charaHandler != null) + { + Mediator.Publish(new PlayerUploadingMessage(_charaHandler, uploading)); + } + } + + public void SetPaused(bool paused) + { + lock (_pauseLock) + { + if (_pauseRequested == paused) + { + return; + } + + _pauseRequested = paused; + _pauseTransitionTask = _pauseTransitionTask + .ContinueWith(_ => paused ? PauseInternalAsync() : ResumeInternalAsync(), TaskScheduler.Default) + .Unwrap(); + } + } + + private void CancelVisibilityGraceTask() + { + lock (_visibilityGraceGate) + { + _visibilityGraceCts?.CancelDispose(); + _visibilityGraceCts = null; + } + } + + private void StartVisibilityGraceTask() + { + CancellationToken token; + lock (_visibilityGraceGate) + { + _visibilityGraceCts = _visibilityGraceCts?.CancelRecreate() ?? new CancellationTokenSource(); + token = _visibilityGraceCts.Token; + } + + _visibilityGraceTask = Task.Run(async () => + { + try + { + await Task.Delay(VisibilityEvictionGrace, token).ConfigureAwait(false); + token.ThrowIfCancellationRequested(); + if (IsVisible) return; + + ScheduledForDeletion = true; + ResetPenumbraCollection(reason: "VisibilityLostTimeout"); + } + catch (OperationCanceledException) + { + // operation cancelled, do nothing + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Visibility grace task failed for {handler}", GetLogIdentifier()); + } + }, CancellationToken.None); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + SetUploading(false); + var name = PlayerName; + var user = GetPrimaryUserDataSafe(); + var alias = GetPrimaryAliasOrUidSafe(); + Logger.LogDebug("Disposing {name} ({user})", name, alias); + try + { + Guid applicationId = Guid.NewGuid(); + _applicationCancellationTokenSource?.CancelDispose(); + _applicationCancellationTokenSource = null; + _downloadCancellationTokenSource?.CancelDispose(); + _downloadCancellationTokenSource = null; + _downloadManager.Dispose(); + _charaHandler?.Dispose(); + CancelVisibilityGraceTask(); + _charaHandler = null; + _invisibleSinceUtc = null; + _visibilityEvictionDueAtUtc = null; + + if (!string.IsNullOrEmpty(name)) + { + Mediator.Publish(new EventMessage(new Event(name, user, nameof(PairHandlerAdapter), 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} ({user})", applicationId, name, alias); + Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, alias); + ResetPenumbraCollection(reason: nameof(Dispose)); + if (!IsVisible) + { + Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, alias); + _ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult(); + } + else + { + using var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(60)); + + var effectiveCachedData = _cachedData ?? _pairStateCache.TryLoad(Ident); + if (effectiveCachedData is not null) + { + _cachedData = effectiveCachedData; + } + + Logger.LogInformation("[{applicationId}] CachedData is null {isNull}, contains things: {contains}", + applicationId, _cachedData == null, _cachedData?.FileReplacements.Any() ?? false); + + foreach (KeyValuePair> 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; + _lastAppliedModdedPaths = null; + _needsCollectionRebuild = false; + _performanceMetricsCache.Clear(Ident); + Logger.LogDebug("Disposing {name} complete", name); + } + } + + private async Task ApplyCustomizationDataAsync(Guid applicationId, KeyValuePair> 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: + if (!ShouldPerformForcedRedraw(changes.Key, changes.Value, charaData)) + { + Logger.LogTrace("[{applicationId}] Skipping forced redraw for {handler}", applicationId, handler); + break; + } + await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false); + break; + + default: + break; + } + token.ThrowIfCancellationRequested(); + } + } + finally + { + if (handler != _charaHandler) handler.Dispose(); + } + } + + private bool ShouldPerformForcedRedraw(ObjectKind objectKind, ICollection changeSet, CharacterData newData) + { + if (objectKind != ObjectKind.Player) + { + return true; + } + + var hasModFiles = changeSet.Contains(PlayerChanges.ModFiles); + var hasManip = changeSet.Contains(PlayerChanges.ModManip); + var modsChanged = hasModFiles && PlayerModFilesChanged(newData, _cachedData); + var manipChanged = hasManip && !string.Equals(_cachedData?.ManipulationData, newData.ManipulationData, StringComparison.Ordinal); + + if (modsChanged) + { + _explicitRedrawQueued = false; + return true; + } + + if (manipChanged) + { + _explicitRedrawQueued = false; + return true; + } + + if (_explicitRedrawQueued) + { + _explicitRedrawQueued = false; + return true; + } + + if ((hasModFiles || hasManip) && (_forceFullReapply || _needsCollectionRebuild)) + { + _explicitRedrawQueued = false; + return true; + } + + return false; + } + + private static Dictionary> BuildFullChangeSet(CharacterData characterData) + { + var result = new Dictionary>(); + + foreach (var objectKind in Enum.GetValues()) + { + var changes = new HashSet(); + + if (characterData.FileReplacements.TryGetValue(objectKind, out var replacements) && replacements.Count > 0) + { + changes.Add(PlayerChanges.ModFiles); + if (objectKind == ObjectKind.Player) + { + changes.Add(PlayerChanges.ForcedRedraw); + } + } + + if (characterData.GlamourerData.TryGetValue(objectKind, out var glamourer) && !string.IsNullOrEmpty(glamourer)) + { + changes.Add(PlayerChanges.Glamourer); + } + + if (characterData.CustomizePlusData.TryGetValue(objectKind, out var customize) && !string.IsNullOrEmpty(customize)) + { + changes.Add(PlayerChanges.Customize); + } + + if (objectKind == ObjectKind.Player) + { + if (!string.IsNullOrEmpty(characterData.ManipulationData)) + { + changes.Add(PlayerChanges.ModManip); + changes.Add(PlayerChanges.ForcedRedraw); + } + + if (!string.IsNullOrEmpty(characterData.HeelsData)) + { + changes.Add(PlayerChanges.Heels); + } + + if (!string.IsNullOrEmpty(characterData.HonorificData)) + { + changes.Add(PlayerChanges.Honorific); + } + + if (!string.IsNullOrEmpty(characterData.MoodlesData)) + { + changes.Add(PlayerChanges.Moodles); + } + + if (!string.IsNullOrEmpty(characterData.PetNamesData)) + { + changes.Add(PlayerChanges.PetNames); + } + } + + if (changes.Count > 0) + { + result[objectKind] = changes; + } + } + + return result; + } + + private static bool PlayerModFilesChanged(CharacterData newData, CharacterData? previousData) + { + return !FileReplacementListsEqual( + TryGetFileReplacementList(newData, ObjectKind.Player), + TryGetFileReplacementList(previousData, ObjectKind.Player)); + } + + private static IReadOnlyCollection? TryGetFileReplacementList(CharacterData? data, ObjectKind objectKind) + { + if (data is null) + { + return null; + } + + return data.FileReplacements.TryGetValue(objectKind, out var list) ? list : null; + } + + private static bool FileReplacementListsEqual(IReadOnlyCollection? left, IReadOnlyCollection? right) + { + if (left is null || left.Count == 0) + { + return right is null || right.Count == 0; + } + + if (right is null || right.Count == 0) + { + return false; + } + + var comparer = FileReplacementDataComparer.Instance; + return !left.Except(right, comparer).Any() && !right.Except(left, comparer).Any(); + } + + private void DownloadAndApplyCharacter(Guid applicationBase, CharacterData charaData, Dictionary> updatedData, bool forceFullReapply) + { + if (!updatedData.Any()) + { + if (forceFullReapply) + { + updatedData = BuildFullChangeSet(charaData); + } + + if (!updatedData.Any()) + { + Logger.LogDebug("[BASE-{appBase}] Nothing to update for {obj}", applicationBase, GetLogIdentifier()); + _forceFullReapply = false; + 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)); + var needsCollectionRebuild = _needsCollectionRebuild; + var reuseCachedModdedPaths = !updateModdedPaths && needsCollectionRebuild && _lastAppliedModdedPaths is not null; + updateModdedPaths = updateModdedPaths || needsCollectionRebuild; + updateManip = updateManip || needsCollectionRebuild; + Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths = null; + if (reuseCachedModdedPaths) + { + if (HasValidCachedModdedPaths()) + { + cachedModdedPaths = _lastAppliedModdedPaths; + } + else + { + Logger.LogDebug("{handler}: Cached files missing, recalculating mappings", GetLogIdentifier()); + _lastAppliedModdedPaths = null; + } + } + + _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource(); + var downloadToken = _downloadCancellationTokenSource.Token; + _ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, cachedModdedPaths, downloadToken) + .ConfigureAwait(false); + } + + private Task? _pairDownloadTask; + private Task _visibilityGraceTask; + + private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary> updatedData, + bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken) + { + var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false); + try + { + bool skipDownscaleForPair = ShouldSkipDownscale(); + var user = GetPrimaryUserData(); + Dictionary<(string GamePath, string? Hash), string> moddedPaths; + + if (updateModdedPaths) + { + if (cachedModdedPaths is not null) + { + moddedPaths = new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer); + } + else + { + int attempts = 0; + List 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, user, nameof(PairHandlerAdapter), EventSeverity.Informational, + $"Starting download for {toDownloadReplacements.Count} files"))); + var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false); + + if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles)) + { + RecordFailure("Auto pause triggered by VRAM usage thresholds", "VRAMThreshold"); + _downloadManager.ClearDownload(); + return; + } + + var handlerForDownload = _charaHandler; + _pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false)); + + await _pairDownloadTask.ConfigureAwait(false); + + if (downloadToken.IsCancellationRequested) + { + Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase); + RecordFailure("Download cancelled", "Cancellation"); + return; + } + + if (!skipDownscaleForPair) + { + var downloadedTextureHashes = toDownloadReplacements + .Where(static replacement => replacement.GamePaths.Any(static path => path.EndsWith(".tex", StringComparison.OrdinalIgnoreCase))) + .Select(static replacement => replacement.Hash) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (downloadedTextureHashes.Count > 0) + { + await _textureDownscaleService.WaitForPendingJobsAsync(downloadedTextureHashes, downloadToken).ConfigureAwait(false); + } + } + + 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)) + { + RecordFailure("Auto pause triggered by performance thresholds", "PerformanceThreshold"); + return; + } + } + } + else + { + moddedPaths = cachedModdedPaths is not null + ? new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer) + : []; + } + + downloadToken.ThrowIfCancellationRequested(); + + var handlerForApply = _charaHandler; + if (handlerForApply is null || handlerForApply.Address == nint.Zero) + { + Logger.LogDebug("[BASE-{appBase}] Handler not available for {player}, cached data for later application", applicationBase, GetLogIdentifier()); + _cachedData = charaData; + _pairStateCache.Store(Ident, charaData); + _forceFullReapply = true; + RecordFailure("Handler not available for application", "HandlerUnavailable"); + return; + } + + var appToken = _applicationCancellationTokenSource?.Token; + while ((!_applicationTask?.IsCompleted ?? false) + && !downloadToken.IsCancellationRequested + && (!appToken?.IsCancellationRequested ?? false)) + { + 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)) + { + _forceFullReapply = true; + RecordFailure("Application cancelled", "Cancellation"); + return; + } + + _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource(); + var token = _applicationCancellationTokenSource.Token; + + _applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token); + } + finally + { + await concurrencyLease.DisposeAsync().ConfigureAwait(false); + } + } + + private async Task ApplyCharacterDataAsync(Guid applicationBase, GameObjectHandler handlerForApply, CharacterData charaData, Dictionary> 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 {handler}: {appId}", applicationBase, GetLogIdentifier(), _applicationId); + + Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply); + await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false); + + token.ThrowIfCancellationRequested(); + + Guid penumbraCollection = Guid.Empty; + if (updateModdedPaths || updateManip) + { + penumbraCollection = EnsurePenumbraCollection(); + if (penumbraCollection == Guid.Empty) + { + Logger.LogTrace("[BASE-{applicationId}] Penumbra collection unavailable for {handler}, caching data for later application", applicationBase, GetLogIdentifier()); + _cachedData = charaData; + _pairStateCache.Store(Ident, charaData); + _forceFullReapply = true; + RecordFailure("Penumbra collection unavailable", "PenumbraUnavailable"); + return; + } + } + + if (updateModdedPaths) + { + // ensure collection is set + var objIndex = await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObject = handlerForApply.GetGameObject(); + return gameObject?.ObjectIndex; + }).ConfigureAwait(false); + + if (!objIndex.HasValue) + { + Logger.LogDebug("[BASE-{applicationId}] GameObject not available for {handler}, caching data for later application", applicationBase, GetLogIdentifier()); + _cachedData = charaData; + _pairStateCache.Store(Ident, charaData); + _forceFullReapply = true; + RecordFailure("Game object not available for application", "GameObjectUnavailable"); + return; + } + + await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, penumbraCollection, objIndex.Value).ConfigureAwait(false); + + await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, penumbraCollection, + moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false); + _lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(moddedPaths, moddedPaths.Comparer); + 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; + _pairStateCache.Store(Ident, charaData); + _forceFullReapply = false; + _needsCollectionRebuild = false; + if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) + { + _playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List()); + } + if (LastAppliedDataTris < 0) + { + await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false); + } + + StorePerformanceMetrics(charaData); + _lastSuccessfulApplyAt = DateTime.UtcNow; + ClearFailureState(); + Logger.LogDebug("[{applicationId}] Application finished", _applicationId); + } + catch (OperationCanceledException) + { + Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier()); + _cachedData = charaData; + _pairStateCache.Store(Ident, charaData); + _forceFullReapply = true; + RecordFailure("Application cancelled", "Cancellation"); + } + catch (Exception ex) + { + if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException)) + { + IsVisible = false; + _forceApplyMods = true; + _cachedData = charaData; + _pairStateCache.Store(Ident, charaData); + _forceFullReapply = true; + Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId); + } + else + { + Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId); + _forceFullReapply = true; + } + RecordFailure($"Application failed: {ex.Message}", "Exception"); + } +} + + private void FrameworkUpdate() + { + if (string.IsNullOrEmpty(PlayerName)) + { + var pc = _dalamudUtil.FindPlayerByNameHash(Ident); + if (pc == default((string, nint))) return; + Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier()); + Initialize(pc.Name); + Logger.LogDebug("One-Time Initialized {handler}", GetLogIdentifier()); + Mediator.Publish(new EventMessage(new Event(PlayerName, GetPrimaryUserData(), nameof(PairHandlerAdapter), EventSeverity.Informational, + $"Initializing User For Character {pc.Name}"))); + } + + if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested) + { + Guid appData = Guid.NewGuid(); + IsVisible = true; + if (_cachedData is not null) + { + var cachedData = _cachedData; + Logger.LogTrace("[BASE-{appBase}] {handler} visibility changed, now: {visi}, cached data exists", appData, GetLogIdentifier(), IsVisible); + + _ = Task.Run(() => + { + try + { + _forceFullReapply = true; + ApplyCharacterData(appData, cachedData!, forceApplyCustomization: true); + } + catch (Exception ex) + { + Logger.LogError(ex, "[BASE-{appBase}] Failed to apply cached character data for {handler}", appData, GetLogIdentifier()); + } + }); + } + else if (LastReceivedCharacterData is not null) + { + Logger.LogTrace("[BASE-{appBase}] {handler} visibility changed, now: {visi}, last received data exists", appData, GetLogIdentifier(), IsVisible); + + _ = Task.Run(() => + { + try + { + _forceFullReapply = true; + ApplyLastReceivedData(forced: true); + } + catch (Exception ex) + { + Logger.LogError(ex, "[BASE-{appBase}] Failed to reapply last received data for {handler}", appData, GetLogIdentifier()); + } + }); + } + else + { + Logger.LogTrace("{handler} visibility changed, now: {visi}, no cached or received data exists", GetLogIdentifier(), IsVisible); + } + } + else if (_charaHandler?.Address == nint.Zero && IsVisible) + { + IsVisible = false; + _charaHandler.Invalidate(); + _downloadCancellationTokenSource?.CancelDispose(); + _downloadCancellationTokenSource = null; + Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible); + } + + TryApplyQueuedData(); + } + + private void Initialize(string name) + { + PlayerName = name; + _charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident), isWatched: false).GetAwaiter().GetResult(); + + var user = GetPrimaryUserData(); + if (!string.IsNullOrEmpty(user.UID)) + { + _serverConfigManager.AutoPopulateNoteForUid(user.UID, name); + } + + Mediator.Subscribe(this, _message => + { + var honorificData = _cachedData?.HonorificData; + if (string.IsNullOrEmpty(honorificData)) + return; + + _ = ReapplyHonorificAsync(honorificData!); + }); + + Mediator.Subscribe(this, _message => + { + var petNamesData = _cachedData?.PetNamesData; + if (string.IsNullOrEmpty(petNamesData)) + return; + + _ = ReapplyPetNamesAsync(petNamesData!); + }); + } + + private async Task ReapplyHonorificAsync(string honorificData) + { + Logger.LogTrace("Reapplying Honorific data for {handler}", GetLogIdentifier()); + await _ipcManager.Honorific.SetTitleAsync(PlayerCharacter, honorificData).ConfigureAwait(false); + } + + private async Task ReapplyPetNamesAsync(string petNamesData) + { + Logger.LogTrace("Reapplying Pet Names data for {handler}", GetLogIdentifier()); + await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, petNamesData).ConfigureAwait(false); + } + + private async Task RevertCustomizationDataAsync(ObjectKind objectKind, string name, Guid applicationId, CancellationToken cancelToken) + { + nint address = _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident); + if (address == nint.Zero) return; + + var alias = GetPrimaryAliasOrUid(); + Logger.LogDebug("[{applicationId}] Reverting all Customization for {alias}/{name} {objectKind}", applicationId, alias, 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, alias, name); + await _ipcManager.Glamourer.RevertAsync(Logger, tempHandler, applicationId, cancelToken).ConfigureAwait(false); + tempHandler.CompareNameAndThrow(name); + Logger.LogDebug("[{applicationId}] Restoring Heels for {alias}/{name}", applicationId, alias, name); + await _ipcManager.Heels.RestoreOffsetForPlayerAsync(address).ConfigureAwait(false); + tempHandler.CompareNameAndThrow(name); + Logger.LogDebug("[{applicationId}] Restoring C+ for {alias}/{name}", applicationId, alias, name); + await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false); + tempHandler.CompareNameAndThrow(name); + Logger.LogDebug("[{applicationId}] Restoring Honorific for {alias}/{name}", applicationId, alias, name); + await _ipcManager.Honorific.ClearTitleAsync(address).ConfigureAwait(false); + Logger.LogDebug("[{applicationId}] Restoring Moodles for {alias}/{name}", applicationId, alias, name); + await _ipcManager.Moodles.RevertStatusAsync(address).ConfigureAwait(false); + Logger.LogDebug("[{applicationId}] Restoring Pet Nicknames for {alias}/{name}", applicationId, alias, 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.Companion, () => 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 TryCalculateModdedDictionary(Guid applicationBase, CharacterData charaData, out Dictionary<(string GamePath, string? Hash), string> moddedDictionary, CancellationToken token) + { + Stopwatch st = Stopwatch.StartNew(); + ConcurrentBag missingFiles = []; + moddedDictionary = []; + ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new(); + bool hasMigrationChanges = false; + bool skipDownscaleForPair = ShouldSkipDownscale(); + + 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 is not null && !File.Exists(fileCache.ResolvedFilepath)) + { + Logger.LogTrace("[BASE-{appBase}] Cached path {Path} missing on disk for hash {Hash}, removing cache entry", applicationBase, fileCache.ResolvedFilepath, item.Hash); + _fileDbManager.RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); + fileCache = null; + } + + 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) + { + var preferredPath = skipDownscaleForPair + ? fileCache.ResolvedFilepath + : _textureDownscaleService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath); + outputDict[(gamePath, item.Hash)] = preferredPath; + } + } + 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 async Task PauseInternalAsync() + { + try + { + Logger.LogDebug("Pausing handler {handler}", GetLogIdentifier()); + DisableSync(); + + if (_charaHandler is null || _charaHandler.Address == nint.Zero) + { + IsVisible = false; + return; + } + + var applicationId = Guid.NewGuid(); + await RevertToRestoredAsync(applicationId).ConfigureAwait(false); + IsVisible = false; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to pause handler {handler}", GetLogIdentifier()); + } + } + + private async Task ResumeInternalAsync() + { + try + { + Logger.LogDebug("Resuming handler {handler}", GetLogIdentifier()); + if (_charaHandler is null || _charaHandler.Address == nint.Zero) + { + return; + } + + if (!IsVisible) + { + IsVisible = true; + } + + if (LastReceivedCharacterData is not null) + { + ApplyLastReceivedData(forced: true); + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to resume handler {handler}", GetLogIdentifier()); + } + } + + private async Task RevertToRestoredAsync(Guid applicationId) + { + if (_charaHandler is null || _charaHandler.Address == nint.Zero) + { + return; + } + + try + { + var gameObject = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler.GetGameObject()).ConfigureAwait(false); + if (gameObject is not Dalamud.Game.ClientState.Objects.Types.ICharacter character) + { + return; + } + + if (_ipcManager.Penumbra.APIAvailable) + { + var penumbraCollection = EnsurePenumbraCollection(); + if (penumbraCollection != Guid.Empty) + { + await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, penumbraCollection, character.ObjectIndex).ConfigureAwait(false); + await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, applicationId, penumbraCollection, new Dictionary(StringComparer.Ordinal)).ConfigureAwait(false); + await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, applicationId, penumbraCollection, string.Empty).ConfigureAwait(false); + } + } + + var kinds = new HashSet(_customizeIds.Keys); + if (_cachedData is not null) + { + foreach (var kind in _cachedData.FileReplacements.Keys) + { + kinds.Add(kind); + } + } + + kinds.Add(ObjectKind.Player); + + var characterName = character.Name.TextValue; + if (string.IsNullOrEmpty(characterName)) + { + characterName = character.Name.ToString(); + } + if (string.IsNullOrEmpty(characterName)) + { + Logger.LogWarning("[{applicationId}] Failed to determine character name for {handler} while reverting", applicationId, GetLogIdentifier()); + return; + } + + foreach (var kind in kinds) + { + await RevertCustomizationDataAsync(kind, characterName, applicationId, CancellationToken.None).ConfigureAwait(false); + } + + _cachedData = null; + LastAppliedDataBytes = -1; + LastAppliedDataTris = -1; + LastAppliedApproximateVRAMBytes = -1; + LastAppliedApproximateEffectiveVRAMBytes = -1; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to revert handler {handler} during pause", GetLogIdentifier()); + } + } + + private void DisableSync() + { + _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate(); + _applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate(); + } + + private void EnableSync() + { + TryApplyQueuedData(); + } + + private void TryApplyQueuedData() + { + var pending = _dataReceivedInDowntime; + if (pending is null || !IsVisible) + { + return; + } + + if (!CanApplyNow()) + { + return; + } + + _dataReceivedInDowntime = null; + ApplyCharacterData(pending.ApplicationId, + pending.CharacterData, pending.Forced); + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs new file mode 100644 index 0000000..1fe2703 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapterFactory.cs @@ -0,0 +1,93 @@ +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(); + return new PairHandlerAdapter( + _loggerFactory.CreateLogger(), + _mediator, + _pairManager, + ident, + _gameObjectHandlerFactory, + _ipcManager, + downloadManager, + _pluginWarningNotificationManager, + dalamudUtilService, + _lifetime, + _fileCacheManager, + _playerPerformanceService, + _pairProcessingLimiter, + _serverConfigManager, + _textureDownscaleService, + _pairStateCache, + _pairPerformanceMetricsCache); + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs b/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs new file mode 100644 index 0000000..ec05ee7 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs @@ -0,0 +1,521 @@ +using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto.User; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.PlayerData.Pairs; + +/// +/// creates, tracks, and removes pair handlers +/// +public sealed class PairHandlerRegistry : IDisposable +{ + private readonly object _gate = new(); + private readonly object _pendingGate = new(); + private readonly object _visibilityGate = new(); + private readonly Dictionary _entriesByIdent = new(StringComparer.Ordinal); + private readonly Dictionary _pendingInvisibleEvictions = new(StringComparer.Ordinal); + private readonly Dictionary _entriesByHandler = new(ReferenceEqualityComparer.Instance); + + private readonly IPairHandlerAdapterFactory _handlerFactory; + private readonly PairManager _pairManager; + private readonly PairStateCache _pairStateCache; + private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache; + private readonly ILogger _logger; + + private readonly TimeSpan _deletionGracePeriod = TimeSpan.FromMinutes(5); + private static readonly TimeSpan _handlerReadyTimeout = TimeSpan.FromMinutes(3); + private const int _handlerReadyPollDelayMs = 500; + private readonly Dictionary _pendingCharacterData = new(StringComparer.Ordinal); + + public PairHandlerRegistry( + IPairHandlerAdapterFactory handlerFactory, + PairManager pairManager, + PairStateCache pairStateCache, + PairPerformanceMetricsCache pairPerformanceMetricsCache, + ILogger 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 RegisterOnlinePair(PairRegistration registration) + { + if (registration.CharacterIdent is null) + { + return PairOperationResult.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.Ok(registration.PairIdent); + } + + public PairOperationResult DeregisterOfflinePair(PairRegistration registration, bool forceDisposal = false) + { + if (registration.CharacterIdent is null) + { + return PairOperationResult.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.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.Ok(registration.PairIdent); + } + + private PairOperationResult CancelAllInvisibleEvictions() + { + List snapshot; + lock (_visibilityGate) + { + snapshot = [.. _pendingInvisibleEvictions.Values]; + _pendingInvisibleEvictions.Clear(); + } + + List? errors = null; + + foreach (var cts in snapshot) + { + try { cts.Cancel(); } + catch (Exception ex) + { + (errors ??= new List()).Add($"Cancel: {ex.Message}"); + } + + try { cts.Dispose(); } + catch (Exception ex) + { + (errors ??= new List()).Add($"Dispose: {ex.Message}"); + } + } + + return errors is null + ? PairOperationResult.Ok() + : PairOperationResult.Fail($"CancelAllInvisibleEvictions had error(s): {string.Join(" | ", errors)}"); + } + + public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto) + { + if (registration.CharacterIdent is null) + { + return PairOperationResult.Fail($"Character data received without ident for {registration.PairIdent.UserId}."); + } + + if (!TryGetHandler(registration.CharacterIdent, out var handler) || handler is null) + { + var registerResult = RegisterOnlinePair(registration); + if (!registerResult.Success) + { + return PairOperationResult.Fail(registerResult.Error); + } + + if (!TryGetHandler(registration.CharacterIdent, out handler) || handler is null) + { + QueuePendingCharacterData(registration, dto); + return PairOperationResult.Ok(); + } + } + + if (!handler.Initialized) + { + handler.Initialize(); + QueuePendingCharacterData(registration, dto); + return PairOperationResult.Ok(); + } + + handler.ApplyData(dto.CharaData); + return PairOperationResult.Ok(); + } + + public PairOperationResult ApplyLastReceivedData(PairUniqueIdentifier pairIdent, string ident, bool forced = false) + { + if (!TryGetHandler(ident, out var handler) || handler is null) + { + return PairOperationResult.Fail($"Cannot reapply data: handler for {pairIdent.UserId} not found."); + } + + handler.ApplyLastReceivedData(forced); + return PairOperationResult.Ok(); + } + + public PairOperationResult SetUploading(PairUniqueIdentifier pairIdent, string ident, bool uploading) + { + if (!TryGetHandler(ident, out var handler) || handler is null) + { + return PairOperationResult.Fail($"Cannot set uploading for {pairIdent.UserId}: handler not found."); + } + + handler.SetUploading(uploading); + return PairOperationResult.Ok(); + } + + public PairOperationResult SetPausedState(PairUniqueIdentifier pairIdent, string ident, bool paused) + { + if (!TryGetHandler(ident, out var handler) || handler is null) + { + return PairOperationResult.Fail($"Cannot update pause state for {pairIdent.UserId}: handler not found."); + } + + _ = paused; // value reflected in pair manager already + ApplyPauseStateForHandler(handler); + return PairOperationResult.Ok(); + } + + public PairOperationResult> GetPairConnections(string ident) + { + PairHandlerEntry? entry; + lock (_gate) + { + _entriesByIdent.TryGetValue(ident, out entry); + } + + if (entry is null) + { + return PairOperationResult>.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>.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 GetHandlerSnapshot() + { + lock (_gate) + { + return _entriesByHandler.Keys.ToList(); + } + } + + internal IReadOnlyCollection GetRegisteredPairs(IPairHandlerAdapter handler) + { + lock (_gate) + { + if (_entriesByHandler.TryGetValue(handler, out var entry)) + { + return entry.SnapshotPairs(); + } + } + + return Array.Empty(); + } + + 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 handlers; + lock (_gate) + { + handlers = _entriesByHandler.Keys.ToList(); + CancelAllInvisibleEvictions(); + _entriesByIdent.Clear(); + _entriesByHandler.Clear(); + } + + CancelAllPendingCharacterData(); + + foreach (var handler in handlers) + { + try + { + handler.Dispose(); + } + catch (Exception ex) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug(ex, "Failed to dispose handler for {Ident}", handler.Ident); + } + } + finally + { + _pairPerformanceMetricsCache.Clear(handler.Ident); + } + } + } + + public void Dispose() + { + List handlers; + lock (_gate) + { + handlers = _entriesByHandler.Keys.ToList(); + CancelAllInvisibleEvictions(); + _entriesByIdent.Clear(); + _entriesByHandler.Clear(); + } + + CancelAllPendingCharacterData(); + + foreach (var handler in handlers) + { + handler.Dispose(); + _pairPerformanceMetricsCache.Clear(handler.Ident); + } + } + + private PairHandlerEntry GetOrCreateEntry(string ident) + { + if (_entriesByIdent.TryGetValue(ident, out var entry)) + { + return entry; + } + + var handler = _handlerFactory.Create(ident); + entry = new PairHandlerEntry(ident, handler); + _entriesByIdent[ident] = entry; + _entriesByHandler[handler] = entry; + return entry; + } + + private async Task RemoveAfterGracePeriodAsync(IPairHandlerAdapter handler) + { + await Task.Delay(_deletionGracePeriod).ConfigureAwait(false); + + if (TryFinalizeHandlerRemoval(handler)) + { + handler.Dispose(); + } + } + + private bool TryFinalizeHandlerRemoval(IPairHandlerAdapter handler) + { + string? ident = null; + lock (_gate) + { + if (!_entriesByHandler.TryGetValue(handler, out var entry) || entry.HasPairs) + { + handler.ScheduledForDeletion = false; + return false; + } + + ident = entry.Ident; + _entriesByHandler.Remove(handler); + _entriesByIdent.Remove(entry.Ident); + } + + if (ident is not null) + { + _pairPerformanceMetricsCache.Clear(ident); + CancelPendingCharacterData(ident); + } + + return true; + } + + private void QueuePendingCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto) + { + if (registration.CharacterIdent is null) return; + + CancellationTokenSource? previous; + CancellationTokenSource cts; + + lock (_pendingGate) + { + _pendingCharacterData.TryGetValue(registration.CharacterIdent, out previous); + previous?.Cancel(); + + cts = new CancellationTokenSource(); + _pendingCharacterData[registration.CharacterIdent] = cts; + } + + cts.CancelAfter(_handlerReadyTimeout); + _ = Task.Run(() => WaitThenApplyPendingCharacterDataAsync(registration, dto, cts.Token, cts)); + } + + private void CancelPendingCharacterData(string ident) + { + CancellationTokenSource? cts = null; + lock (_pendingGate) + { + if (_pendingCharacterData.TryGetValue(ident, out cts)) + _pendingCharacterData.Remove(ident); + } + + cts?.Cancel(); + } + + private void CancelAllPendingCharacterData() + { + List? 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(); + } + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairLedger.cs b/LightlessSync/PlayerData/Pairs/PairLedger.cs new file mode 100644 index 0000000..b151e1f --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairLedger.cs @@ -0,0 +1,294 @@ +using LightlessSync.API.Dto.Group; +using LightlessSync.Services.Mediator; +using LightlessSync.UI.Models; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.PlayerData.Pairs; + +/// +/// keeps pair info for ui and reapplication +/// +public sealed class PairLedger : DisposableMediatorSubscriberBase +{ + private readonly PairManager _pairManager; + private readonly PairHandlerRegistry _registry; + private readonly ILogger _logger; + private readonly object _metricsGate = new(); + private CancellationTokenSource? _ensureMetricsCts; + + public PairLedger( + ILogger logger, + LightlessMediator mediator, + PairManager pairManager, + PairHandlerRegistry registry) : base(logger, mediator) + { + _pairManager = pairManager; + _registry = registry; + _logger = logger; + + Mediator.Subscribe(this, _ => ReapplyAll(forced: true)); + Mediator.Subscribe(this, _ => ReapplyAll()); + Mediator.Subscribe(this, _ => ReapplyAll(forced: true)); + Mediator.Subscribe(this, _ => ReapplyAll(forced: true)); + Mediator.Subscribe(this, _ => Reset()); + Mediator.Subscribe(this, _ => ScheduleEnsureMetrics(TimeSpan.FromSeconds(2))); + Mediator.Subscribe(this, _ => ScheduleEnsureMetrics(TimeSpan.FromSeconds(2))); + Mediator.Subscribe(this, _ => ScheduleEnsureMetrics(TimeSpan.FromSeconds(2))); + Mediator.Subscribe(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 GetVisiblePairs() + { + return _pairManager.GetAllPairs() + .Select(kv => kv.Value) + .Where(connection => connection.Ident is not null && _registry.IsIdentVisible(connection.Ident)) + .ToList(); + } + + public IReadOnlyList GetAllGroupInfos() + { + return _pairManager.GetAllGroups() + .Select(kv => kv.Value.GroupFullInfo) + .ToList(); + } + + public IReadOnlyDictionary 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 GetAllEntries() + { + var groups = _pairManager.GetAllGroups(); + var list = new List(); + 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() + .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() + .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); + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairManager.cs b/LightlessSync/PlayerData/Pairs/PairManager.cs index 2044db0..eb70a54 100644 --- a/LightlessSync/PlayerData/Pairs/PairManager.cs +++ b/LightlessSync/PlayerData/Pairs/PairManager.cs @@ -1,497 +1,577 @@ -using Dalamud.Plugin.Services; +using System.Diagnostics.CodeAnalysis; using LightlessSync.API.Data; -using LightlessSync.API.Data.Comparer; -using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Data.Enum; using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; -using LightlessSync.LightlessConfiguration; -using LightlessSync.LightlessConfiguration.Models; -using LightlessSync.PlayerData.Factories; -using LightlessSync.Services; - -using LightlessSync.Services.Events; -using LightlessSync.Services.Mediator; -using Microsoft.Extensions.Logging; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; namespace LightlessSync.PlayerData.Pairs; -public sealed class PairManager : DisposableMediatorSubscriberBase +/// +/// in memory state for pairs, groups, and syncshells +/// +public sealed class PairManager { - private readonly ConcurrentDictionary _allClientPairs = new(UserDataComparer.Instance); - private readonly ConcurrentDictionary _allGroups = new(GroupDataComparer.Instance); - private readonly LightlessConfigService _configurationService; - private readonly IContextMenu _dalamudContextMenu; - private readonly PairFactory _pairFactory; - private Lazy> _directPairsInternal; - private Lazy>> _groupPairsInternal; - private Lazy>> _pairsWithGroupsInternal; - private readonly PairProcessingLimiter _pairProcessingLimiter; - private readonly ConcurrentQueue<(Pair Pair, OnlineUserIdentDto? Ident)> _pairCreationQueue = new(); - private CancellationTokenSource _pairCreationCts = new(); - private int _pairCreationProcessorRunning; + private readonly object _gate = new(); + private readonly Dictionary _pairs = new(StringComparer.Ordinal); + private readonly Dictionary _groups = new(StringComparer.Ordinal); - public PairManager(ILogger logger, PairFactory pairFactory, - LightlessConfigService configurationService, LightlessMediator mediator, - IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter) : base(logger, mediator) + public PairConnection? LastAddedUser { get; private set; } + + public IReadOnlyDictionary GetAllPairs() { - _pairFactory = pairFactory; - _configurationService = configurationService; - _dalamudContextMenu = dalamudContextMenu; - _pairProcessingLimiter = pairProcessingLimiter; - Mediator.Subscribe(this, (_) => ClearPairs()); - Mediator.Subscribe(this, (_) => ReapplyPairData()); - _directPairsInternal = DirectPairsLazy(); - _groupPairsInternal = GroupPairsLazy(); - _pairsWithGroupsInternal = PairsWithGroupsLazy(); - - _dalamudContextMenu.OnMenuOpened += DalamudContextMenuOnOnOpenGameObjectContextMenu; - } - - public List DirectPairs => _directPairsInternal.Value; - - public Dictionary> GroupPairs => _groupPairsInternal.Value; - public Dictionary Groups => _allGroups.ToDictionary(k => k.Key, k => k.Value); - public Pair? LastAddedUser { get; internal set; } - public Dictionary> PairsWithGroups => _pairsWithGroupsInternal.Value; - - public void AddGroup(GroupFullInfoDto dto) - { - _allGroups[dto.Group] = dto; - RecreateLazy(); - } - - public void AddGroupPair(GroupPairFullInfoDto dto) - { - if (!_allClientPairs.ContainsKey(dto.User)) - _allClientPairs[dto.User] = _pairFactory.Create(new UserFullPairDto(dto.User, API.Data.Enum.IndividualPairStatus.None, - [dto.Group.GID], dto.SelfToOtherPermissions, dto.OtherToSelfPermissions)); - else _allClientPairs[dto.User].UserPair.Groups.Add(dto.GID); - RecreateLazy(); - } - - public Pair? GetPairByUID(string uid) - { - var existingPair = _allClientPairs.FirstOrDefault(f => f.Key.UID == uid); - if (!Equals(existingPair, default(KeyValuePair))) + lock (_gate) { - return existingPair.Value; + return new Dictionary(_pairs); } - - return null; } - public void AddUserPair(UserFullPairDto dto) + public IReadOnlyDictionary GetAllGroups() { - if (!_allClientPairs.ContainsKey(dto.User)) + lock (_gate) { - _allClientPairs[dto.User] = _pairFactory.Create(dto); + return new Dictionary(_groups); } - else - { - _allClientPairs[dto.User].UserPair.IndividualPairStatus = dto.IndividualPairStatus; - _allClientPairs[dto.User].ApplyLastReceivedData(); - } - - RecreateLazy(); } - public void AddUserPair(UserPairDto dto, bool addToLastAddedUser = true) + public PairConnection? GetLastAddedUser() { - if (!_allClientPairs.ContainsKey(dto.User)) + lock (_gate) { - _allClientPairs[dto.User] = _pairFactory.Create(dto); + return LastAddedUser; } - else - { - addToLastAddedUser = false; - } - - _allClientPairs[dto.User].UserPair.IndividualPairStatus = dto.IndividualPairStatus; - _allClientPairs[dto.User].UserPair.OwnPermissions = dto.OwnPermissions; - _allClientPairs[dto.User].UserPair.OtherPermissions = dto.OtherPermissions; - if (addToLastAddedUser) - LastAddedUser = _allClientPairs[dto.User]; - _allClientPairs[dto.User].ApplyLastReceivedData(); - RecreateLazy(); } - public void ClearPairs() + public void ClearLastAddedUser() { - Logger.LogDebug("Clearing all Pairs"); - ResetPairCreationQueue(); - DisposePairs(); - _allClientPairs.Clear(); - _allGroups.Clear(); - RecreateLazy(); - } - - public List GetOnlineUserPairs() => _allClientPairs.Where(p => !string.IsNullOrEmpty(p.Value.GetPlayerNameHash())).Select(p => p.Value).ToList(); - - public int GetVisibleUserCount() => _allClientPairs.Count(p => p.Value.IsVisible); - - public List GetVisibleUsers() => [.. _allClientPairs.Where(p => p.Value.IsVisible).Select(p => p.Key)]; - - public void MarkPairOffline(UserData user) - { - if (_allClientPairs.TryGetValue(user, out var pair)) + lock (_gate) { - Mediator.Publish(new ClearProfileUserDataMessage(pair.UserData)); - pair.MarkOffline(); + LastAddedUser = null; } - - RecreateLazy(); } - public void MarkPairOnline(OnlineUserIdentDto dto, bool sendNotif = true) + public void ClearAll() { - if (!_allClientPairs.ContainsKey(dto.User)) throw new InvalidOperationException("No user found for " + dto); - - Mediator.Publish(new ClearProfileUserDataMessage(dto.User)); - - var pair = _allClientPairs[dto.User]; - if (pair.HasCachedPlayer) + lock (_gate) { - RecreateLazy(); - return; + _pairs.Clear(); + _groups.Clear(); + LastAddedUser = null; } - - if (sendNotif && _configurationService.Current.ShowOnlineNotifications - && (_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs && pair.IsDirectlyPaired && !pair.IsOneSidedPair - || !_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs) - && (_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs && !string.IsNullOrEmpty(pair.GetNote()) - || !_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs)) - { - string? note = pair.GetNote(); - var msg = !string.IsNullOrEmpty(note) - ? $"{note} ({pair.UserData.AliasOrUID}) is now online" - : $"{pair.UserData.AliasOrUID} is now online"; - Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5))); - } - - QueuePairCreation(pair, dto); - - RecreateLazy(); } - public void ReceiveCharaData(OnlineUserCharaDataDto dto) + public PairOperationResult GetPair(string userId) { - if (!_allClientPairs.TryGetValue(dto.User, out var pair)) throw new InvalidOperationException("No user found for " + dto.User); - - Mediator.Publish(new EventMessage(new Event(pair.UserData, nameof(PairManager), EventSeverity.Informational, "Received Character Data"))); - _allClientPairs[dto.User].ApplyData(dto); - } - - public void RemoveGroup(GroupData data) - { - _allGroups.TryRemove(data, out _); - - foreach (var item in _allClientPairs.ToList()) + lock (_gate) { - item.Value.UserPair.Groups.Remove(data.GID); - - if (!item.Value.HasAnyConnection()) + if (_pairs.TryGetValue(userId, out var connection)) { - item.Value.MarkOffline(); - _allClientPairs.TryRemove(item.Key, out _); - } - } - - RecreateLazy(); - } - - public void RemoveGroupPair(GroupPairDto dto) - { - if (_allClientPairs.TryGetValue(dto.User, out var pair)) - { - pair.UserPair.Groups.Remove(dto.Group.GID); - - if (!pair.HasAnyConnection()) - { - pair.MarkOffline(); - _allClientPairs.TryRemove(dto.User, out _); - } - } - - RecreateLazy(); - } - - public void RemoveUserPair(UserDto dto) - { - if (_allClientPairs.TryGetValue(dto.User, out var pair)) - { - pair.UserPair.IndividualPairStatus = API.Data.Enum.IndividualPairStatus.None; - - if (!pair.HasAnyConnection()) - { - pair.MarkOffline(); - _allClientPairs.TryRemove(dto.User, out _); - } - } - - RecreateLazy(); - } - - public void SetGroupInfo(GroupInfoDto dto) - { - _allGroups[dto.Group].Group = dto.Group; - _allGroups[dto.Group].Owner = dto.Owner; - _allGroups[dto.Group].GroupPermissions = dto.GroupPermissions; - - RecreateLazy(); - } - - public void UpdatePairPermissions(UserPermissionsDto dto) - { - if (!_allClientPairs.TryGetValue(dto.User, out var pair)) - { - throw new InvalidOperationException("No such pair for " + dto); - } - - if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto); - - if (pair.UserPair.OtherPermissions.IsPaused() != dto.Permissions.IsPaused()) - { - Mediator.Publish(new ClearProfileUserDataMessage(dto.User)); - } - - pair.UserPair.OtherPermissions = dto.Permissions; - - Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}", - pair.UserPair.OtherPermissions.IsPaused(), - pair.UserPair.OtherPermissions.IsDisableAnimations(), - pair.UserPair.OtherPermissions.IsDisableSounds(), - pair.UserPair.OtherPermissions.IsDisableVFX()); - - if (!pair.IsPaused) - pair.ApplyLastReceivedData(); - - RecreateLazy(); - } - - public void UpdateSelfPairPermissions(UserPermissionsDto dto) - { - if (!_allClientPairs.TryGetValue(dto.User, out var pair)) - { - throw new InvalidOperationException("No such pair for " + dto); - } - - if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused()) - { - Mediator.Publish(new ClearProfileUserDataMessage(dto.User)); - } - - pair.UserPair.OwnPermissions = dto.Permissions; - - Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}", - pair.UserPair.OwnPermissions.IsPaused(), - pair.UserPair.OwnPermissions.IsDisableAnimations(), - pair.UserPair.OwnPermissions.IsDisableSounds(), - pair.UserPair.OwnPermissions.IsDisableVFX()); - - if (!pair.IsPaused) - pair.ApplyLastReceivedData(); - - RecreateLazy(); - } - - internal void ReceiveUploadStatus(UserDto dto) - { - if (_allClientPairs.TryGetValue(dto.User, out var existingPair) && existingPair.IsVisible) - { - existingPair.SetIsUploading(); - } - } - - internal void SetGroupPairStatusInfo(GroupPairUserInfoDto dto) - { - _allGroups[dto.Group].GroupPairUserInfos[dto.UID] = dto.GroupUserInfo; - RecreateLazy(); - } - - internal void SetGroupPermissions(GroupPermissionDto dto) - { - _allGroups[dto.Group].GroupPermissions = dto.Permissions; - RecreateLazy(); - } - - internal void SetGroupStatusInfo(GroupPairUserInfoDto dto) - { - _allGroups[dto.Group].GroupUserInfo = dto.GroupUserInfo; - RecreateLazy(); - } - - internal void UpdateGroupPairPermissions(GroupPairUserPermissionDto dto) - { - _allGroups[dto.Group].GroupUserPermissions = dto.GroupPairPermissions; - RecreateLazy(); - } - - internal void UpdateIndividualPairStatus(UserIndividualPairStatusDto dto) - { - if (_allClientPairs.TryGetValue(dto.User, out var pair)) - { - pair.UserPair.IndividualPairStatus = dto.IndividualPairStatus; - RecreateLazy(); - } - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - ResetPairCreationQueue(); - _dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu; - - DisposePairs(); - } - - private void DalamudContextMenuOnOnOpenGameObjectContextMenu(Dalamud.Game.Gui.ContextMenu.IMenuOpenedArgs args) - { - if (args.MenuType == Dalamud.Game.Gui.ContextMenu.ContextMenuType.Inventory) return; - if (!_configurationService.Current.EnableRightClickMenus) return; - - foreach (var pair in _allClientPairs.Where((p => p.Value.IsVisible))) - { - pair.Value.AddContextMenu(args); - } - } - - private Lazy> DirectPairsLazy() => new(() => _allClientPairs.Select(k => k.Value) - .Where(k => k.IndividualPairStatus != API.Data.Enum.IndividualPairStatus.None).ToList()); - - private void DisposePairs() - { - Logger.LogDebug("Disposing all Pairs"); - Parallel.ForEach(_allClientPairs, item => - { - item.Value.MarkOffline(wait: false); - }); - - RecreateLazy(); - } - - private Lazy>> GroupPairsLazy() - { - return new Lazy>>(() => - { - Dictionary> outDict = []; - foreach (var group in _allGroups) - { - outDict[group.Value] = _allClientPairs.Select(p => p.Value).Where(p => p.UserPair.Groups.Exists(g => GroupDataComparer.Instance.Equals(group.Key, new(g)))).ToList(); - } - return outDict; - }); - } - - private Lazy>> PairsWithGroupsLazy() - { - return new Lazy>>(() => - { - Dictionary> outDict = []; - - foreach (var pair in _allClientPairs.Select(k => k.Value)) - { - outDict[pair] = _allGroups.Where(k => pair.UserPair.Groups.Contains(k.Key.GID, StringComparer.Ordinal)).Select(k => k.Value).ToList(); + return PairOperationResult.Ok(connection); } - return outDict; - }); - } - - private void QueuePairCreation(Pair pair, OnlineUserIdentDto? dto) - { - if (pair.HasCachedPlayer) - { - RecreateLazy(); - return; - } - - _pairCreationQueue.Enqueue((pair, dto)); - StartPairCreationProcessor(); - } - - private void StartPairCreationProcessor() - { - if (_pairCreationCts.IsCancellationRequested) - { - return; - } - - if (Interlocked.CompareExchange(ref _pairCreationProcessorRunning, 1, 0) == 0) - { - _ = Task.Run(ProcessPairCreationQueueAsync); + return PairOperationResult.Fail($"Pair {userId} not found."); } } - private async Task ProcessPairCreationQueueAsync() + public bool TryGetPair(string userId, [NotNullWhen(true)] out PairConnection? connection) { - try + lock (_gate) { - while (!_pairCreationCts.IsCancellationRequested) + return _pairs.TryGetValue(userId, out connection); + } + } + + public PairOperationResult GetGroup(string groupId) + { + lock (_gate) + { + if (_groups.TryGetValue(groupId, out var shell)) { - if (!_pairCreationQueue.TryDequeue(out var work)) + return PairOperationResult.Ok(shell); + } + + return PairOperationResult.Fail($"Group {groupId} not found."); + } + } + + public IReadOnlyList GetDirectPairs() + { + lock (_gate) + { + return _pairs.Values.Where(p => p.IsDirectlyPaired).ToList(); + } + } + + public IReadOnlyList GetPairsByIdent(string ident) + { + lock (_gate) + { + return _pairs.Values + .Where(p => p.Ident is not null && string.Equals(p.Ident, ident, StringComparison.Ordinal)) + .ToList(); + } + } + + public IReadOnlyList GetOwnedOrModeratedShells(string currentUserUid) + { + lock (_gate) + { + return _groups.Values + .Where(s => + string.Equals(s.GroupFullInfo.Owner.UID, currentUserUid, StringComparison.OrdinalIgnoreCase) + || s.GroupFullInfo.GroupUserInfo.HasFlag(GroupPairUserInfo.IsModerator)) + .ToList(); + } + } + + public PairOperationResult GetPairCombinedPermissions(string userId) + { + lock (_gate) + { + if (!_pairs.TryGetValue(userId, out var connection)) + { + return PairOperationResult.Fail($"Pair {userId} not found."); + } + + var combined = connection.SelfToOtherPermissions | connection.OtherToSelfPermissions; + return PairOperationResult.Ok(combined); + } + } + + public PairOperationResult MarkOnline(OnlineUserIdentDto dto) + { + lock (_gate) + { + if (!_pairs.TryGetValue(dto.User.UID, out var connection)) + { + connection = GetOrCreatePair(dto.User); + } + + connection.SetOnline(dto.Ident); + return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), dto.Ident)); + } + } + + public PairOperationResult MarkOffline(UserData user) + { + lock (_gate) + { + if (!_pairs.TryGetValue(user.UID, out var connection)) + { + return PairOperationResult.Fail($"Pair {user.UID} not found."); + } + + connection.SetOffline(); + return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(user.UID), connection.Ident)); + } + } + + public PairOperationResult AddOrUpdateIndividual(UserPairDto dto, bool markAsLastAddedUser = true) + { + lock (_gate) + { + var connection = GetOrCreatePair(dto.User, out var created); + connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions); + connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus); + + if (connection.Ident is null) + { + return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), null)); + } + + if (created && markAsLastAddedUser) + { + LastAddedUser = connection; + } + + return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident)); + } + } + + public PairOperationResult AddOrUpdateIndividual(UserFullPairDto dto) + { + lock (_gate) + { + var connection = GetOrCreatePair(dto.User, out _); + connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions); + connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus); + + var removedGroups = connection.Groups.Keys.Where(k => !dto.Groups.Contains(k, StringComparer.Ordinal)).ToList(); + foreach (var groupId in removedGroups) + { + connection.RemoveGroupRelationship(groupId); + if (_groups.TryGetValue(groupId, out var shell)) { - break; + shell.Users.Remove(dto.User.UID); } - - try - { - await using var lease = await _pairProcessingLimiter.AcquireAsync(_pairCreationCts.Token).ConfigureAwait(false); - if (!work.Pair.HasCachedPlayer) - { - work.Pair.CreateCachedPlayer(work.Ident); - } - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - Logger.LogError(ex, "Error creating cached player for {uid}", work.Pair.UserData.UID); - } - - RecreateLazy(); - await Task.Yield(); } - } - finally - { - Interlocked.Exchange(ref _pairCreationProcessorRunning, 0); - if (!_pairCreationQueue.IsEmpty && !_pairCreationCts.IsCancellationRequested) + + foreach (var groupId in dto.Groups) { - StartPairCreationProcessor(); + connection.EnsureGroupRelationship(groupId, null); + if (_groups.TryGetValue(groupId, out var shell)) + { + shell.Users[dto.User.UID] = connection; + } } + + return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident)); } } - private void ResetPairCreationQueue() + public PairOperationResult RemoveIndividual(UserDto dto) { - _pairCreationCts.Cancel(); - while (_pairCreationQueue.TryDequeue(out _)) + lock (_gate) { + if (!_pairs.TryGetValue(dto.User.UID, out var connection)) + { + return PairOperationResult.Fail($"Pair {dto.User.UID} not found."); + } + + connection.UpdateStatus(null); + var registration = TryRemovePairIfNoConnection(connection); + return PairOperationResult.Ok(registration); } - _pairCreationCts.Dispose(); - _pairCreationCts = new CancellationTokenSource(); - Interlocked.Exchange(ref _pairCreationProcessorRunning, 0); } - private void ReapplyPairData() + public PairOperationResult SetPairOtherToSelfPermissions(UserPermissionsDto dto) { - foreach (var pair in _allClientPairs.Select(k => k.Value)) + lock (_gate) { - pair.ApplyLastReceivedData(forced: true); + if (!_pairs.TryGetValue(dto.User.UID, out var connection)) + { + return PairOperationResult.Fail($"Pair {dto.User.UID} not found."); + } + + connection.UpdatePermissions(connection.SelfToOtherPermissions, dto.Permissions); + return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident)); } } - private void RecreateLazy() + public PairOperationResult SetPairSelfToOtherPermissions(UserPermissionsDto dto) { - _directPairsInternal = DirectPairsLazy(); - _groupPairsInternal = GroupPairsLazy(); - _pairsWithGroupsInternal = PairsWithGroupsLazy(); - Mediator.Publish(new RefreshUiMessage()); + lock (_gate) + { + if (!_pairs.TryGetValue(dto.User.UID, out var connection)) + { + return PairOperationResult.Fail($"Pair {dto.User.UID} not found."); + } + + connection.UpdatePermissions(dto.Permissions, connection.OtherToSelfPermissions); + return PairOperationResult.Ok(new PairRegistration(new PairUniqueIdentifier(dto.User.UID), connection.Ident)); + } } -} \ No newline at end of file + + public PairOperationResult SetIndividualStatus(UserIndividualPairStatusDto dto) + { + lock (_gate) + { + if (!_pairs.TryGetValue(dto.User.UID, out var connection)) + { + return PairOperationResult.Fail($"Pair {dto.User.UID} not found."); + } + + connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus); + _ = TryRemovePairIfNoConnection(connection); + return PairOperationResult.Ok(); + } + } + + public PairOperationResult AddOrUpdateGroupPair(GroupPairFullInfoDto dto) + { + lock (_gate) + { + var shell = GetOrCreateShell(dto.Group); + var connection = GetOrCreatePair(dto.User); + + var groupInfo = shell.GroupFullInfo.GroupPairUserInfos.GetValueOrDefault(dto.User.UID, GroupPairUserInfo.None); + connection.EnsureGroupRelationship(dto.Group.GID, groupInfo == GroupPairUserInfo.None ? null : groupInfo); + connection.UpdatePermissions(dto.SelfToOtherPermissions, dto.OtherToSelfPermissions); + + shell.Users[dto.User.UID] = connection; + return PairOperationResult.Ok(); + } + } + + public PairOperationResult RemoveGroupPair(GroupPairDto dto) + { + lock (_gate) + { + if (_groups.TryGetValue(dto.GID, out var shell)) + { + shell.Users.Remove(dto.User.UID); + } + + PairRegistration? registration = null; + if (_pairs.TryGetValue(dto.User.UID, out var connection)) + { + connection.RemoveGroupRelationship(dto.GID); + registration = TryRemovePairIfNoConnection(connection); + } + + return PairOperationResult.Ok(registration); + } + } + + public PairOperationResult> RemoveGroup(string groupId) + { + lock (_gate) + { + if (!_groups.Remove(groupId, out var shell)) + { + return PairOperationResult>.Fail($"Group {groupId} not found."); + } + + var removed = new List(); + foreach (var connection in shell.Users.Values.ToList()) + { + connection.RemoveGroupRelationship(groupId); + var registration = TryRemovePairIfNoConnection(connection); + if (registration is not null) + { + removed.Add(registration); + } + } + + return PairOperationResult>.Ok(removed); + } + } + + public PairOperationResult AddGroup(GroupFullInfoDto dto) + { + lock (_gate) + { + if (!_groups.TryGetValue(dto.Group.GID, out var shell)) + { + shell = new Syncshell(dto); + _groups[dto.Group.GID] = shell; + } + else + { + shell.Update(dto); + shell.Users.Clear(); + } + + foreach (var (userId, info) in dto.GroupPairUserInfos) + { + if (_pairs.TryGetValue(userId, out var connection)) + { + connection.EnsureGroupRelationship(dto.Group.GID, info == GroupPairUserInfo.None ? null : info); + shell.Users[userId] = connection; + } + } + + return PairOperationResult.Ok(); + } + } + + public PairOperationResult UpdateGroupInfo(GroupInfoDto dto) + { + lock (_gate) + { + if (!_groups.TryGetValue(dto.Group.GID, out var shell)) + { + return PairOperationResult.Fail($"Group {dto.Group.GID} not found."); + } + + var updated = new GroupFullInfoDto( + dto.Group, + dto.Owner, + dto.GroupPermissions, + shell.GroupFullInfo.GroupUserPermissions, + shell.GroupFullInfo.GroupUserInfo, + new Dictionary(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal), + 0); + + shell.Update(updated); + return PairOperationResult.Ok(); + } + } + + public PairOperationResult UpdateGroupPairPermissions(GroupPairUserPermissionDto dto) + { + lock (_gate) + { + if (!_groups.TryGetValue(dto.Group.GID, out var shell)) + { + return PairOperationResult.Fail($"Group {dto.Group.GID} not found."); + } + + var updated = shell.GroupFullInfo with { GroupUserPermissions = dto.GroupPairPermissions }; + shell.Update(updated); + return PairOperationResult.Ok(); + } + } + + public PairOperationResult UpdateGroupPermissions(GroupPermissionDto dto) + { + lock (_gate) + { + if (!_groups.TryGetValue(dto.Group.GID, out var shell)) + { + return PairOperationResult.Fail($"Group {dto.Group.GID} not found."); + } + + var updated = shell.GroupFullInfo with { GroupPermissions = dto.Permissions }; + shell.Update(updated); + return PairOperationResult.Ok(); + } + } + + public PairOperationResult UpdateGroupPairStatus(GroupPairUserInfoDto dto) + { + lock (_gate) + { + if (_pairs.TryGetValue(dto.UID, out var connection)) + { + connection.EnsureGroupRelationship(dto.GID, dto.GroupUserInfo == GroupPairUserInfo.None ? null : dto.GroupUserInfo); + } + + if (_groups.TryGetValue(dto.GID, out var shell)) + { + var infos = new Dictionary(shell.GroupFullInfo.GroupPairUserInfos, StringComparer.Ordinal) + { + [dto.UID] = dto.GroupUserInfo + }; + var updated = shell.GroupFullInfo with { GroupPairUserInfos = infos }; + shell.Update(updated); + } + + return PairOperationResult.Ok(); + } + } + + public PairOperationResult UpdateGroupStatus(GroupPairUserInfoDto dto) + { + lock (_gate) + { + if (!_groups.TryGetValue(dto.GID, out var shell)) + { + return PairOperationResult.Fail($"Group {dto.GID} not found."); + } + + var updated = shell.GroupFullInfo with { GroupUserInfo = dto.GroupUserInfo }; + shell.Update(updated); + return PairOperationResult.Ok(); + } + } + + public PairOperationResult UpdateOtherPermissions(UserPermissionsDto dto) + { + lock (_gate) + { + if (!_pairs.TryGetValue(dto.User.UID, out var connection)) + { + return PairOperationResult.Fail($"Pair {dto.User.UID} not found."); + } + + connection.UpdatePermissions(connection.SelfToOtherPermissions, dto.Permissions); + return PairOperationResult.Ok(); + } + } + + public PairOperationResult UpdateSelfPermissions(UserPermissionsDto dto) + { + lock (_gate) + { + if (!_pairs.TryGetValue(dto.User.UID, out var connection)) + { + return PairOperationResult.Fail($"Pair {dto.User.UID} not found."); + } + + connection.UpdatePermissions(dto.Permissions, connection.OtherToSelfPermissions); + return PairOperationResult.Ok(); + } + } + + private PairConnection GetOrCreatePair(UserData user) + { + return GetOrCreatePair(user, out _); + } + + private PairConnection GetOrCreatePair(UserData user, out bool created) + { + if (_pairs.TryGetValue(user.UID, out var connection)) + { + created = false; + return connection; + } + + connection = new PairConnection(user); + _pairs[user.UID] = connection; + created = true; + return connection; + } + + private Syncshell GetOrCreateShell(GroupData group) + { + if (_groups.TryGetValue(group.GID, out var shell)) + { + return shell; + } + + var placeholder = new GroupFullInfoDto( + group, + new UserData(string.Empty), + GroupPermissions.NoneSet, + GroupUserPreferredPermissions.NoneSet, + GroupPairUserInfo.None, + new Dictionary(StringComparer.Ordinal), + 0); + + shell = new Syncshell(placeholder); + _groups[group.GID] = shell; + return shell; + } + + private PairRegistration? TryRemovePairIfNoConnection(PairConnection connection) + { + if (connection.HasAnyConnection) + { + return null; + } + + if (connection.IsOnline) + { + connection.SetOffline(); + } + + var userId = connection.User.UID; + _pairs.Remove(userId); + foreach (var shell in _groups.Values) + { + shell.Users.Remove(userId); + } + + return new PairRegistration(new PairUniqueIdentifier(userId), connection.Ident); + } + + public static PairConnection CreateFromFullData(UserFullPairDto dto) + { + var connection = new PairConnection(dto.User); + connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions); + connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus); + + foreach (var groupId in dto.Groups) + { + connection.EnsureGroupRelationship(groupId, null); + } + + return connection; + } + + public static PairConnection CreateFromPartialData(UserPairDto dto) + { + var connection = new PairConnection(dto.User); + connection.UpdatePermissions(dto.OwnPermissions, dto.OtherPermissions); + connection.UpdateStatus(dto.IndividualPairStatus == IndividualPairStatus.None ? null : dto.IndividualPairStatus); + return connection; + } + + public static GroupPairRelationship CreateGroupPairRelationshipFromFullInfo(string userUid, GroupFullInfoDto fullInfo) + { + return new GroupPairRelationship(fullInfo.Group.GID, + fullInfo.GroupPairUserInfos.TryGetValue(userUid, out var info) && info != GroupPairUserInfo.None + ? info + : null); + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairModels.cs b/LightlessSync/PlayerData/Pairs/PairModels.cs new file mode 100644 index 0000000..9f34ab2 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairModels.cs @@ -0,0 +1,220 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto.Group; + +namespace LightlessSync.PlayerData.Pairs; + +/// +/// core models for the pair system +/// +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); + +/// +/// link between a pair id and character ident +/// +public sealed record PairRegistration(PairUniqueIdentifier PairIdent, string? CharacterIdent); + +/// +/// per group membership info for a pair +/// +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; + } +} + +/// +/// runtime view of a single pair connection +/// +public sealed class PairConnection +{ + public PairConnection(UserData user) + { + User = user; + Groups = new Dictionary(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 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); + } +} + +/// +/// syncshell metadata plus member connections +/// +public sealed class Syncshell +{ + public Syncshell(GroupFullInfoDto dto) + { + GroupFullInfo = dto; + Users = new Dictionary(StringComparer.Ordinal); + } + + public GroupFullInfoDto GroupFullInfo { get; private set; } + public Dictionary Users { get; } + + public void Update(GroupFullInfoDto dto) + { + GroupFullInfo = dto; + } +} + +/// +/// simple success/failure result +/// +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); +} + +/// +/// typed success/failure result +/// +public readonly struct PairOperationResult +{ + 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 Ok(T value) => new(true, value, null); + + public static PairOperationResult Fail(string error) => new(false, default!, error); +} + +/// +/// state of which optional plugin warnings were shown +/// +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; +} + +/// +/// tracks the handler registered pairs for an ident +/// +internal sealed class PairHandlerEntry +{ + private readonly HashSet _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 SnapshotPairs() + { + if (_pairs.Count == 0) + { + return Array.Empty(); + } + + return _pairs.ToArray(); + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairPerformanceMetricsCache.cs b/LightlessSync/PlayerData/Pairs/PairPerformanceMetricsCache.cs new file mode 100644 index 0000000..110d845 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairPerformanceMetricsCache.cs @@ -0,0 +1,65 @@ +using System.Collections.Concurrent; + +namespace LightlessSync.PlayerData.Pairs; + +public readonly record struct PairPerformanceMetrics( + long TriangleCount, + long ApproximateVramBytes, + long ApproximateEffectiveVramBytes); + +/// +/// caches performance metrics keyed by pair ident +/// +public sealed class PairPerformanceMetricsCache +{ + private sealed record CacheEntry(string DataHash, PairPerformanceMetrics Metrics); + + private readonly ConcurrentDictionary _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(); + } +} diff --git a/LightlessSync/PlayerData/Pairs/PairStateCache.cs b/LightlessSync/PlayerData/Pairs/PairStateCache.cs new file mode 100644 index 0000000..3d7a377 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairStateCache.cs @@ -0,0 +1,119 @@ +using System.Collections.Concurrent; +using LightlessSync.API.Data; +using LightlessSync.Utils; + +namespace LightlessSync.PlayerData.Pairs; + +/// +/// cache for character/pair data and penumbra collections +/// +public sealed class PairStateCache +{ + private readonly ConcurrentDictionary _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 ClearAllTemporaryCollections() + { + var removed = new List(); + 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 _); + } + } +} diff --git a/LightlessSync/PlayerData/Pairs/VisibleUserDataDistributor.cs b/LightlessSync/PlayerData/Pairs/VisibleUserDataDistributor.cs index 6ead2ca..f71080a 100644 --- a/LightlessSync/PlayerData/Pairs/VisibleUserDataDistributor.cs +++ b/LightlessSync/PlayerData/Pairs/VisibleUserDataDistributor.cs @@ -1,4 +1,5 @@ using LightlessSync.API.Data; +using LightlessSync.API.Data.Comparer; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Utils; @@ -8,27 +9,29 @@ using Microsoft.Extensions.Logging; namespace LightlessSync.PlayerData.Pairs; +/// +/// pushes character data to visible pairs +/// public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase { private readonly ApiController _apiController; private readonly DalamudUtilService _dalamudUtil; private readonly FileUploadManager _fileTransferManager; - private readonly PairManager _pairManager; + private readonly PairLedger _pairLedger; private CharacterData? _lastCreatedData; private CharacterData? _uploadingCharacterData = null; private readonly List _previouslyVisiblePlayers = []; private Task? _fileUploadTask = null; - private readonly HashSet _usersToPushDataTo = []; - private readonly SemaphoreSlim _pushDataSemaphore = new(1, 1); + private readonly HashSet _usersToPushDataTo = new(UserDataComparer.Instance); + private readonly SemaphoreSlim _pushLock = new(1, 1); private readonly CancellationTokenSource _runtimeCts = new(); - public VisibleUserDataDistributor(ILogger logger, ApiController apiController, DalamudUtilService dalamudUtil, - PairManager pairManager, LightlessMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator) + PairLedger pairLedger, LightlessMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator) { _apiController = apiController; _dalamudUtil = dalamudUtil; - _pairManager = pairManager; + _pairLedger = pairLedger; _fileTransferManager = fileTransferManager; Mediator.Subscribe(this, (_) => FrameworkOnUpdate()); Mediator.Subscribe(this, (msg) => @@ -47,7 +50,14 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase }); Mediator.Subscribe(this, (_) => PushToAllVisibleUsers()); - Mediator.Subscribe(this, (_) => _previouslyVisiblePlayers.Clear()); + Mediator.Subscribe(this, (_) => + { + _fileTransferManager.CancelUpload(); + _previouslyVisiblePlayers.Clear(); + _usersToPushDataTo.Clear(); + _uploadingCharacterData = null; + _fileUploadTask = null; + }); } protected override void Dispose(bool disposing) @@ -63,7 +73,7 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase private void PushToAllVisibleUsers(bool forced = false) { - foreach (var user in _pairManager.GetVisibleUsers()) + foreach (var user in GetVisibleUsers()) { _usersToPushDataTo.Add(user); } @@ -79,8 +89,8 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase { if (!_dalamudUtil.GetIsPlayerPresent() || !_apiController.IsConnected) return; - var allVisibleUsers = _pairManager.GetVisibleUsers(); - var newVisibleUsers = allVisibleUsers.Except(_previouslyVisiblePlayers).ToList(); + var allVisibleUsers = GetVisibleUsers(); + var newVisibleUsers = allVisibleUsers.Except(_previouslyVisiblePlayers, UserDataComparer.Instance).ToList(); _previouslyVisiblePlayers.Clear(); _previouslyVisiblePlayers.AddRange(allVisibleUsers); if (newVisibleUsers.Count == 0) return; @@ -98,46 +108,49 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase private void PushCharacterData(bool forced = false) { if (_lastCreatedData == null || _usersToPushDataTo.Count == 0) return; + _ = PushCharacterDataAsync(forced); + } - _ = Task.Run(async () => + private async Task PushCharacterDataAsync(bool forced = false) + { + await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false); + try { - try - { - forced |= _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash; + if (_lastCreatedData == null || _usersToPushDataTo.Count == 0) + return; - if (_fileUploadTask == null || (_fileUploadTask?.IsCompleted ?? false) || forced) + var hashChanged = _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash; + forced |= hashChanged; + + if (_fileUploadTask == null || _fileUploadTask.IsCompleted || 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, [.. _usersToPushDataTo]); + _lastCreatedData.DataHash, + _fileUploadTask == null, + _fileUploadTask?.IsCompleted ?? false, + forced); + + _fileUploadTask = _fileTransferManager.UploadFiles(_uploadingCharacterData, uploadTargets); } - 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(); - } - } + 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(); } - catch (OperationCanceledException) when (_runtimeCts.IsCancellationRequested) + finally { - Logger.LogDebug("PushCharacterData cancelled"); + _pushLock.Release(); } - catch (Exception ex) - { - Logger.LogError(ex, "Failed to push character data"); - } - }); } -} \ No newline at end of file + + private List GetVisibleUsers() => [.. _pairLedger.GetVisiblePairs().Select(connection => connection.User)]; +} diff --git a/LightlessSync/PlayerData/Services/CacheCreationService.cs b/LightlessSync/PlayerData/Services/CacheCreationService.cs index 9c64d8f..69a975e 100644 --- a/LightlessSync/PlayerData/Services/CacheCreationService.cs +++ b/LightlessSync/PlayerData/Services/CacheCreationService.cs @@ -20,6 +20,7 @@ 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; @@ -183,7 +184,18 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase { if (_isZoning || _haltCharaDataCreation) return; - if (_cachesToCreate.Count == 0) return; + bool hasCaches; + _cacheCreateLock.Wait(); + try + { + hasCaches = _cachesToCreate.Count > 0; + } + finally + { + _cacheCreateLock.Release(); + } + + if (!hasCaches) return; if (_playerRelatedObjects.Any(p => p.Value.CurrentDrawCondition is not (GameObjectHandler.DrawCondition.None or GameObjectHandler.DrawCondition.DrawObjectZero or GameObjectHandler.DrawCondition.ObjectZero))) @@ -197,6 +209,11 @@ 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); @@ -225,8 +242,17 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase _playerData.SetFragment(kvp.Key, kvp.Value); } - Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI())); - _currentlyCreating.Clear(); + 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)); + } } catch (OperationCanceledException) { @@ -238,6 +264,7 @@ public sealed class CacheCreationService : DisposableMediatorSubscriberBase } finally { + _currentlyCreating.Clear(); Logger.LogDebug("Cache Creation complete"); } }); diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 01a4de4..58374e3 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -1,5 +1,6 @@ using Dalamud.Game; using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; using Dalamud.Plugin; @@ -13,14 +14,20 @@ 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; @@ -30,6 +37,9 @@ 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; @@ -43,6 +53,7 @@ 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"); @@ -85,218 +96,459 @@ public sealed class Plugin : IDalamudPlugin }); lb.SetMinimumLevel(LogLevel.Trace); }) - .ConfigureServices(collection => + .ConfigureServices(services => + { + var configDir = pluginInterface.ConfigDirectory.FullName; + + // Core infrastructure + services.AddSingleton(new WindowSystem("LightlessSync")); + services.AddSingleton(); + services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true)); + services.AddSingleton(gameGui); + services.AddSingleton(addonLifecycle); + services.AddSingleton(pluginInterface.UiBuilder); + + // Core singletons + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(sp => + new TextureMetadataHelper(sp.GetRequiredService>(), gameData)); + + services.AddSingleton(sp => new Lazy(() => sp.GetRequiredService())); + + services.AddSingleton(sp => new PairFactory( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + new Lazy(() => sp.GetRequiredService()), + sp.GetRequiredService>())); + + services.AddSingleton(sp => new TransientResourceManager( + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + // Lightless Chara data + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Game / VFX / IPC + services.AddSingleton(sp => new VfxSpawnManager( + sp.GetRequiredService>(), + gameInteropProvider, + sp.GetRequiredService())); + + services.AddSingleton(sp => new BlockedCharacterHandler( + sp.GetRequiredService>(), + gameInteropProvider)); + + services.AddSingleton(sp => new IpcProvider( + sp.GetRequiredService>(), + pluginInterface, + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddSingleton(sp => new PictomancyService( + sp.GetRequiredService>(), + pluginInterface)); + + // Tag (Groups) UIs + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Eventing / utilities + services.AddSingleton(sp => new EventAggregator( + configDir, + sp.GetRequiredService>(), + sp.GetRequiredService())); + + services.AddSingleton(sp => new ActorObjectService( + sp.GetRequiredService>(), + framework, + gameInteropProvider, + objectTable, + clientState, + sp.GetRequiredService())); + + services.AddSingleton(sp => new DalamudUtilService( + sp.GetRequiredService>(), + clientState, + objectTable, + framework, + gameGui, + condition, + gameData, + targetManager, + gameConfig, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + new Lazy(() => sp.GetRequiredService()))); + + // Pairing and Dtr integration + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(sp => new PairHandlerRegistry( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>())); + + services.AddSingleton(sp => new DtrEntry( + sp.GetRequiredService>(), + dtrBar, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddSingleton(sp => new PairCoordinator( + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + // Light finder / redraw / context menu + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(sp => new LightFinderPlateHandler( + sp.GetRequiredService>(), + addonLifecycle, + gameGui, + sp.GetRequiredService(), + sp.GetRequiredService(), + objectTable, + sp.GetRequiredService(), + pluginInterface, + sp.GetRequiredService())); + + services.AddSingleton(sp => new LightFinderScannerService( + sp.GetRequiredService>(), + framework, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddSingleton(sp => new ContextMenuService( + contextMenu, + pluginInterface, + gameData, + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + objectTable, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + clientState, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + // IPC callers / manager + services.AddSingleton(sp => new IpcCallerPenumbra( + sp.GetRequiredService>(), + pluginInterface, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddSingleton(sp => new IpcCallerGlamourer( + sp.GetRequiredService>(), + pluginInterface, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddSingleton(sp => new IpcCallerCustomize( + sp.GetRequiredService>(), + pluginInterface, + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddSingleton(sp => new IpcCallerHeels( + sp.GetRequiredService>(), + pluginInterface, + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddSingleton(sp => new IpcCallerHonorific( + sp.GetRequiredService>(), + pluginInterface, + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddSingleton(sp => new IpcCallerMoodles( + sp.GetRequiredService>(), + pluginInterface, + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddSingleton(sp => new IpcCallerPetNames( + sp.GetRequiredService>(), + pluginInterface, + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddSingleton(sp => new IpcCallerBrio( + sp.GetRequiredService>(), + pluginInterface, + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddSingleton(sp => new IpcManager( + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + // Notifications / HTTP + services.AddSingleton(sp => new NotificationService( + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + notificationManager, + chatGui, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddSingleton(sp => { - collection.AddSingleton(new WindowSystem("LightlessSync")); - collection.AddSingleton(); - collection.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", "", useEmbedded: true)); - collection.AddSingleton(gameGui); + 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; + }); - // add lightless related singletons - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(s => new Lazy(() => s.GetRequiredService())); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); + // 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(); + 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)); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); + // Config adapters + services.AddSingleton>(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); - collection.AddSingleton(s => new VfxSpawnManager(s.GetRequiredService>(), - gameInteropProvider, s.GetRequiredService())); - collection.AddSingleton((s) => new BlockedCharacterHandler(s.GetRequiredService>(), gameInteropProvider)); - collection.AddSingleton((s) => new IpcProvider(s.GetRequiredService>(), - pluginInterface, - s.GetRequiredService(), - s.GetRequiredService())); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton((s) => new EventAggregator(pluginInterface.ConfigDirectory.FullName, - s.GetRequiredService>(), s.GetRequiredService())); - collection.AddSingleton((s) => new DalamudUtilService(s.GetRequiredService>(), - clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig, - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService())); - collection.AddSingleton((s) => new DtrEntry( - s.GetRequiredService>(), - dtrBar, - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService())); - collection.AddSingleton(s => new PairManager(s.GetRequiredService>(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), contextMenu, s.GetRequiredService())); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(addonLifecycle); - collection.AddSingleton(p => new ContextMenuService(contextMenu, pluginInterface, gameData, - p.GetRequiredService>(), p.GetRequiredService(), p.GetRequiredService(), objectTable, - p.GetRequiredService(), p.GetRequiredService(), p.GetRequiredService(), clientState)); - collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService>(), pluginInterface, - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService>(), pluginInterface, - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddSingleton((s) => new IpcCallerCustomize(s.GetRequiredService>(), pluginInterface, - s.GetRequiredService(), s.GetRequiredService())); - collection.AddSingleton((s) => new IpcCallerHeels(s.GetRequiredService>(), pluginInterface, - s.GetRequiredService(), s.GetRequiredService())); - collection.AddSingleton((s) => new IpcCallerHonorific(s.GetRequiredService>(), pluginInterface, - s.GetRequiredService(), s.GetRequiredService())); - collection.AddSingleton((s) => new IpcCallerMoodles(s.GetRequiredService>(), pluginInterface, - s.GetRequiredService(), s.GetRequiredService())); - collection.AddSingleton((s) => new IpcCallerPetNames(s.GetRequiredService>(), pluginInterface, - s.GetRequiredService(), s.GetRequiredService())); - collection.AddSingleton((s) => new IpcCallerBrio(s.GetRequiredService>(), pluginInterface, - s.GetRequiredService())); - collection.AddSingleton((s) => new IpcManager(s.GetRequiredService>(), - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddSingleton((s) => new NotificationService( - s.GetRequiredService>(), - s.GetRequiredService(), - s.GetRequiredService(), - notificationManager, - chatGui, - s.GetRequiredService(), - s.GetRequiredService())); - 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) => new UiThemeConfigService(pluginInterface.ConfigDirectory.FullName)); - collection.AddSingleton((s) => - { - var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName); - var theme = s.GetRequiredService(); - LightlessSync.UI.Style.MainStyle.Init(cfg, theme); - return cfg; - }); - collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); - collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName)); - collection.AddSingleton((s) => new 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>(s => s.GetRequiredService()); - collection.AddSingleton>(s => s.GetRequiredService()); - collection.AddSingleton>(s => s.GetRequiredService()); - collection.AddSingleton>(s => s.GetRequiredService()); - collection.AddSingleton>(s => s.GetRequiredService()); - collection.AddSingleton>(s => s.GetRequiredService()); - collection.AddSingleton>(s => s.GetRequiredService()); - collection.AddSingleton>(s => s.GetRequiredService()); - collection.AddSingleton>(s => s.GetRequiredService()); - collection.AddSingleton>(s => s.GetRequiredService()); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService>(), clientState, objectTable, framework, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + services.AddSingleton(); + services.AddSingleton(); + // Scoped factories / UI + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); - // add scoped services - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); + services.AddScoped(sp => new EditProfileUi( + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); - collection.AddScoped((s) => new EditProfileUi(s.GetRequiredService>(), - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped(); - collection.AddScoped((s) => new BroadcastUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped((s) => new SyncshellFinderUI(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped(); - collection.AddScoped((s) => - new LightlessNotificationUi( - s.GetRequiredService>(), - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService())); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped(); - collection.AddScoped((s) => new UiService(s.GetRequiredService>(), pluginInterface.UiBuilder, s.GetRequiredService(), - s.GetRequiredService(), s.GetServices(), - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService())); - collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped((s) => new UiSharedService(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - pluginInterface, textureProvider, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService())); - collection.AddScoped((s) => new NameplateService(s.GetRequiredService>(), s.GetRequiredService(), namePlateGui, clientState, - s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService>(), addonLifecycle, gameGui, s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), clientState, s.GetRequiredService())); + services.AddScoped(); - collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); - collection.AddHostedService(p => p.GetRequiredService()); - }) - .Build(); + services.AddScoped(sp => new LightFinderUI( + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddScoped(sp => new SyncshellFinderUI( + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(sp => + new LightlessNotificationUi( + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(sp => new UiService( + sp.GetRequiredService>(), + pluginInterface.UiBuilder, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetServices(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddScoped(sp => new CommandManagerService( + commandManager, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddScoped(sp => new UiSharedService( + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + pluginInterface, + textureProvider, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + services.AddScoped(sp => new NameplateService( + sp.GetRequiredService>(), + sp.GetRequiredService(), + clientState, + gameGui, + objectTable, + gameInteropProvider, + sp.GetRequiredService(), + sp.GetRequiredService())); + + // Hosted services + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + services.AddHostedService(sp => sp.GetRequiredService()); + }).Build(); _ = _host.StartAsync(); } diff --git a/LightlessSync/Services/ActorTracking/ActorObjectService.cs b/LightlessSync/Services/ActorTracking/ActorObjectService.cs new file mode 100644 index 0000000..c76d6dd --- /dev/null +++ b/LightlessSync/Services/ActorTracking/ActorObjectService.cs @@ -0,0 +1,938 @@ +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 _logger; + private readonly IFramework _framework; + private readonly IGameInteropProvider _interop; + private readonly IObjectTable _objectTable; + private readonly LightlessMediator _mediator; + + private readonly ConcurrentDictionary _activePlayers = new(); + private readonly ConcurrentDictionary _actorsByHash = new(StringComparer.Ordinal); + private readonly ConcurrentDictionary> _actorsByName = new(StringComparer.Ordinal); + private readonly OwnedObjectTracker _ownedTracker = new(); + private ActorSnapshot _snapshot = ActorSnapshot.Empty; + + private Hook? _onInitializeHook; + private Hook? _onTerminateHook; + private Hook? _onDestructorHook; + private Hook? _onCompanionInitializeHook; + private Hook? _onCompanionTerminateHook; + + private bool _hooksActive; + private static readonly TimeSpan SnapshotRefreshInterval = TimeSpan.FromSeconds(1); + private DateTime _nextRefreshAllowed = DateTime.MinValue; + + public ActorObjectService( + ILogger 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 PlayerAddresses => Snapshot.PlayerAddresses; + + public IEnumerable PlayerDescriptors => _activePlayers.Values; + public IReadOnlyList 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 RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers; + public IReadOnlyList RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions; + public IReadOnlyList OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses; + public IReadOnlyDictionary 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( + (nint)Character.StaticVirtualTablePointer->OnInitialize, + OnCharacterInitialized); + + _onTerminateHook = _interop.HookFromAddress( + (nint)Character.StaticVirtualTablePointer->Terminate, + OnCharacterTerminated); + + _onDestructorHook = _interop.HookFromAddress( + (nint)Character.StaticVirtualTablePointer->Dtor, + OnCharacterDisposed); + + _onCompanionInitializeHook = _interop.HookFromAddress( + (nint)Companion.StaticVirtualTablePointer->OnInitialize, + OnCompanionInitialized); + + _onCompanionTerminateHook = _interop.HookFromAddress( + (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() ?? "", + 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() ?? ""); + } + + _mediator.Publish(new ActorUntrackedMessage(descriptor)); + } + } + + private unsafe void RefreshTrackedActorsInternal() + { + var addresses = EnumerateActiveCharacterAddresses(); + HashSet 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()); + 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 EnumerateActiveCharacterAddresses() + { + var results = new List(64); + var manager = GameObjectManager.Instance(); + if (manager == null) + return results; + + const int objectLimit = 200; + + unsafe + { + for (var i = 0; i < objectLimit; i++) + { + Pointer 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 _renderedPlayers = new(); + private readonly HashSet _renderedCompanions = new(); + private readonly Dictionary _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(_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 RenderedPlayers, + IReadOnlyList RenderedCompanions, + IReadOnlyList OwnedAddresses, + IReadOnlyDictionary Map, + nint LocalPlayer, + nint LocalPet, + nint LocalMinionOrMount, + nint LocalCompanion) + { + public static OwnedObjectSnapshot Empty { get; } = new( + Array.Empty(), + Array.Empty(), + Array.Empty(), + new Dictionary(), + nint.Zero, + nint.Zero, + nint.Zero, + nint.Zero); + } + + private sealed record ActorSnapshot( + IReadOnlyList PlayerDescriptors, + IReadOnlyList PlayerAddresses, + OwnedObjectSnapshot OwnedObjects, + int Generation) + { + public static ActorSnapshot Empty { get; } = new( + Array.Empty(), + Array.Empty(), + OwnedObjectSnapshot.Empty, + 0); + } +} diff --git a/LightlessSync/Services/CharaData/CharaDataManager.cs b/LightlessSync/Services/CharaData/CharaDataManager.cs index 38ec1c7..d8b2387 100644 --- a/LightlessSync/Services/CharaData/CharaDataManager.cs +++ b/LightlessSync/Services/CharaData/CharaDataManager.cs @@ -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 _nearbyData = []; private readonly CharaDataNearbyManager _nearbyManager; private readonly CharaDataCharacterHandler _characterHandler; - private readonly PairManager _pairManager; + private readonly PairUiService _pairUiService; private readonly Dictionary _ownCharaData = []; private readonly Dictionary _sharedMetaInfoTimeoutTasks = []; private readonly Dictionary> _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, PairManager pairManager) : base(logger, lightlessMediator) + CharaDataCharacterHandler charaDataCharacterHandler, PairUiService pairUiService) : base(logger, lightlessMediator) { _apiController = apiController; _fileHandler = charaDataFileHandler; @@ -54,7 +54,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase _configService = charaDataConfigService; _nearbyManager = charaDataNearbyManager; _characterHandler = charaDataCharacterHandler; - _pairManager = pairManager; + _pairUiService = pairUiService; lightlessMediator.Subscribe(this, (msg) => { _connectCts?.Cancel(); @@ -421,9 +421,10 @@ 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)) { - var pair = _pairManager.GetPairByUID(grouping.Key.UID); + snapshot.PairsByUid.TryGetValue(grouping.Key.UID, out var pair); if (pair?.IsPaused ?? false) continue; List newList = new(); foreach (var item in grouping) diff --git a/LightlessSync/Services/CharaData/CharacterAnalysisSummary.cs b/LightlessSync/Services/CharaData/CharacterAnalysisSummary.cs new file mode 100644 index 0000000..0eaf312 --- /dev/null +++ b/LightlessSync/Services/CharaData/CharacterAnalysisSummary.cs @@ -0,0 +1,19 @@ +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.Empty); + + internal CharacterAnalysisSummary(IImmutableDictionary objects) + { + Objects = objects; + } + + public IImmutableDictionary Objects { get; } + + public bool HasData => Objects.Any(kvp => kvp.Value.HasEntries); +} \ No newline at end of file diff --git a/LightlessSync/Services/CharaData/Models/CharacterAnalysisObjectSummary.cs b/LightlessSync/Services/CharaData/Models/CharacterAnalysisObjectSummary.cs new file mode 100644 index 0000000..aa42394 --- /dev/null +++ b/LightlessSync/Services/CharaData/Models/CharacterAnalysisObjectSummary.cs @@ -0,0 +1,8 @@ +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; +} diff --git a/LightlessSync/Services/CharacterAnalyzer.cs b/LightlessSync/Services/CharacterAnalyzer.cs index 27235f6..3eebced 100644 --- a/LightlessSync/Services/CharacterAnalyzer.cs +++ b/LightlessSync/Services/CharacterAnalyzer.cs @@ -1,16 +1,14 @@ -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.Generic; using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; namespace LightlessSync.Services; public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable @@ -40,73 +38,97 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable public int TotalFiles { get; internal set; } internal Dictionary> 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(); - if (allFiles.Exists(c => !c.IsComputed || recalculate)) + + 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 { - 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 + foreach (var file in remaining) { - foreach (var file in remaining) - { - Logger.LogDebug("Computing file {file}", file.FilePaths[0]); - await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false); - CurrentFile++; - } + cancelToken.ThrowIfCancellationRequested(); - _fileCacheManager.WriteOutFullCsv(); + var path = file.FilePaths.FirstOrDefault() ?? ""; + Logger.LogDebug("Computing file {file}", path); + await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false); + + CurrentFile++; } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to analyze files"); - } - finally - { - Mediator.Publish(new ResumeScanMessage(nameof(CharacterAnalyzer))); - } + + 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))); } RecalculateSummary(); - Mediator.Publish(new CharacterDataAnalyzedMessage()); - _analysisCts.CancelDispose(); _analysisCts = null; - if (print) PrintAnalysis(); } - public void Dispose() { _analysisCts.CancelDispose(); + _baseAnalysisCts.Dispose(); + } + public async Task UpdateFileEntriesAsync(IEnumerable filePaths, CancellationToken token) + { + var normalized = new HashSet( + 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 data = new(StringComparer.OrdinalIgnoreCase); @@ -114,9 +136,8 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { token.ThrowIfCancellationRequested(); - var fileCacheEntries = _fileCacheManager.GetAllFileCachesByHash(fileEntry.Hash, ignoreCacheEntries: true, validate: false).ToList(); + var fileCacheEntries = (await _fileCacheManager.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false)).ToList(); if (fileCacheEntries.Count == 0) continue; - var filePath = fileCacheEntries[0].ResolvedFilepath; FileInfo fi = new(filePath); string ext = "unk?"; @@ -128,30 +149,24 @@ 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().ToList(), + [.. fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal)], 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(); @@ -177,7 +192,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable _latestSummary = new CharacterAnalysisSummary(builder.ToImmutable()); } - private void PrintAnalysis() { if (LastAnalysis.Count == 0) return; @@ -186,7 +200,6 @@ 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); @@ -215,7 +228,6 @@ 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), @@ -223,7 +235,6 @@ 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 GamePaths, List FilePaths, long OriginalSize, long CompressedSize, long Triangles) { public bool IsComputed => OriginalSize > 0 && CompressedSize > 0; @@ -231,7 +242,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 = fileCacheManager.GetAllFileCachesByHash(Hash, ignoreCacheEntries: true, validate: false); + var entries = await fileCacheManager.GetAllFileCachesByHashAsync(Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false); foreach (var entry in entries) { entry.Size = normalSize; @@ -239,53 +250,40 @@ 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 Format => _format ??= CreateFormatValue(); - public Lazy Format = new(() => + private Lazy? _format; + + public void RefreshFormat() { - switch (FileType) + _format = CreateFormatValue(); + } + + private Lazy CreateFormatValue() + => new(() => { - 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: + if (!string.Equals(FileType, "tex", StringComparison.Ordinal)) + { 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"; + } + }); } } - -public readonly record struct CharacterAnalysisObjectSummary(int EntryCount, long TotalTriangles, long TexOriginalBytes, long TexCompressedBytes) -{ - public bool HasEntries => EntryCount > 0; -} - -public sealed class CharacterAnalysisSummary -{ - public static CharacterAnalysisSummary Empty { get; } = - new(ImmutableDictionary.Empty); - - internal CharacterAnalysisSummary(IImmutableDictionary objects) - { - Objects = objects; - } - - public IImmutableDictionary Objects { get; } - - public bool HasData => Objects.Any(kvp => kvp.Value.HasEntries); -} \ No newline at end of file diff --git a/LightlessSync/Services/Chat/ChatModels.cs b/LightlessSync/Services/Chat/ChatModels.cs new file mode 100644 index 0000000..0f35f7c --- /dev/null +++ b/LightlessSync/Services/Chat/ChatModels.cs @@ -0,0 +1,34 @@ +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 Messages); + +public readonly record struct ChatReportResult(bool Success, string? ErrorMessage); \ No newline at end of file diff --git a/LightlessSync/Services/Chat/ZoneChatService.cs b/LightlessSync/Services/Chat/ZoneChatService.cs new file mode 100644 index 0000000..6eebf4f --- /dev/null +++ b/LightlessSync/Services/Chat/ZoneChatService.cs @@ -0,0 +1,1315 @@ +using LightlessSync.API.Dto.Chat; +using LightlessSync.Services.ActorTracking; +using LightlessSync.Services.Mediator; +using LightlessSync.WebAPI; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using LightlessSync.UI.Services; +using LightlessSync.LightlessConfiguration; + +namespace LightlessSync.Services.Chat; + +public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedService +{ + private const int MaxMessageHistory = 150; + internal const int MaxOutgoingLength = 200; + private const int MaxUnreadCount = 999; + private const string ZoneUnavailableMessage = "Zone chat is only available in major cities."; + private const string ZoneChannelKey = "zone"; + private const int MaxReportReasonLength = 100; + private const int MaxReportContextLength = 1000; + + private readonly ApiController _apiController; + private readonly DalamudUtilService _dalamudUtilService; + private readonly ActorObjectService _actorObjectService; + private readonly PairUiService _pairUiService; + private readonly ChatConfigService _chatConfigService; + + private readonly Lock _sync = new(); + + private readonly Dictionary _channels = new(StringComparer.Ordinal); + private readonly List _channelOrder = new(); + private readonly Dictionary _territoryToZoneKey = new(); + private readonly Dictionary _zoneDefinitions = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _groupDefinitions = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _lastReadCounts = new(StringComparer.Ordinal); + private readonly Dictionary _lastPresenceStates = new(StringComparer.Ordinal); + private readonly Dictionary _selfTokens = new(StringComparer.Ordinal); + private readonly List _pendingSelfMessages = new(); + + private bool _isLoggedIn; + private bool _isConnected; + private ChatChannelDescriptor? _lastZoneDescriptor; + private string? _activeChannelKey; + private bool _chatEnabled = false; + private bool _chatHandlerRegistered; + + public ZoneChatService( + ILogger logger, + LightlessMediator mediator, + ChatConfigService chatConfigService, + ApiController apiController, + DalamudUtilService dalamudUtilService, + ActorObjectService actorObjectService, + PairUiService pairUiService) + : base(logger, mediator) + { + _apiController = apiController; + _dalamudUtilService = dalamudUtilService; + _actorObjectService = actorObjectService; + _pairUiService = pairUiService; + _chatConfigService = chatConfigService; + + _isLoggedIn = _dalamudUtilService.IsLoggedIn; + _isConnected = _apiController.IsConnected; + _chatEnabled = chatConfigService.Current.AutoEnableChatOnLogin; + } + + public IReadOnlyList GetChannelsSnapshot() + { + using (_sync.EnterScope()) + { + var snapshots = new List(_channelOrder.Count); + foreach (var key in _channelOrder) + { + if (!_channels.TryGetValue(key, out var state)) + continue; + + var statusText = state.StatusText; + if (!_chatEnabled) + { + statusText = "Chat services disabled"; + } + else if (!_isConnected) + { + statusText = "Disconnected from chat server"; + } + + snapshots.Add(new ChatChannelSnapshot( + state.Key, + state.Descriptor, + state.DisplayName, + state.Type, + state.IsConnected, + state.IsConnected && state.IsAvailable, + statusText, + state.HasUnread, + state.UnreadCount, + state.Messages.ToList())); + } + + return snapshots; + } + } + + public bool IsChatEnabled + { + get + { + using (_sync.EnterScope()) + { + return _chatEnabled; + } + } + } + + public bool IsChatConnected + { + get + { + using (_sync.EnterScope()) + { + return _chatEnabled && _isConnected; + } + } + } + + public void SetActiveChannel(string? key) + { + using (_sync.EnterScope()) + { + _activeChannelKey = key; + if (key is not null && _channels.TryGetValue(key, out var state)) + { + state.HasUnread = false; + state.UnreadCount = 0; + _lastReadCounts[key] = state.Messages.Count; + } + } + } + + public void MoveChannel(string draggedKey, string targetKey) + { + if (string.IsNullOrWhiteSpace(draggedKey) || string.IsNullOrWhiteSpace(targetKey)) + { + return; + } + + bool updated = false; + using (_sync.EnterScope()) + { + if (!_channels.ContainsKey(draggedKey) || !_channels.ContainsKey(targetKey)) + { + return; + } + + var fromIndex = _channelOrder.IndexOf(draggedKey); + var toIndex = _channelOrder.IndexOf(targetKey); + if (fromIndex < 0 || toIndex < 0 || fromIndex == toIndex) + { + return; + } + + _channelOrder.RemoveAt(fromIndex); + var insertIndex = Math.Clamp(toIndex, 0, _channelOrder.Count); + _channelOrder.Insert(insertIndex, draggedKey); + _chatConfigService.Current.ChannelOrder = new List(_channelOrder); + _chatConfigService.Save(); + updated = true; + } + + if (updated) + { + PublishChannelListChanged(); + } + } + + public Task SetChatEnabledAsync(bool enabled) + => enabled ? EnableChatAsync() : DisableChatAsync(); + + private async Task EnableChatAsync() + { + bool wasEnabled; + using (_sync.EnterScope()) + { + wasEnabled = _chatEnabled; + if (!wasEnabled) + { + _chatEnabled = true; + } + } + + if (wasEnabled) + return; + + RegisterChatHandler(); + + await RefreshChatChannelDefinitionsAsync().ConfigureAwait(false); + ScheduleZonePresenceUpdate(force: true); + await EnsureGroupPresenceAsync(force: true).ConfigureAwait(false); + } + + private async Task DisableChatAsync() + { + bool wasEnabled; + List groupDescriptors; + ChatChannelDescriptor? zoneDescriptor; + + using (_sync.EnterScope()) + { + wasEnabled = _chatEnabled; + if (!wasEnabled) + { + return; + } + + _chatEnabled = false; + zoneDescriptor = _lastZoneDescriptor; + _lastZoneDescriptor = null; + + groupDescriptors = _channels.Values + .Where(state => state.Type == ChatChannelType.Group) + .Select(state => state.Descriptor) + .ToList(); + + _selfTokens.Clear(); + _pendingSelfMessages.Clear(); + + foreach (var state in _channels.Values) + { + state.IsConnected = false; + state.IsAvailable = false; + state.StatusText = "Chat services disabled"; + } + } + + UnregisterChatHandler(); + + if (zoneDescriptor.HasValue) + { + await SendPresenceAsync(zoneDescriptor.Value, 0, isActive: false, force: true).ConfigureAwait(false); + } + + foreach (var descriptor in groupDescriptors) + { + await SendPresenceAsync(descriptor, 0, isActive: false, force: true).ConfigureAwait(false); + } + + PublishChannelListChanged(); + } + + public async Task SendMessageAsync(ChatChannelDescriptor descriptor, string message) + { + if (!_chatEnabled) + return false; + + if (string.IsNullOrWhiteSpace(message)) + return false; + + var sanitized = message.Trim().ReplaceLineEndings(" "); + if (sanitized.Length == 0) + return false; + + if (sanitized.Length > MaxOutgoingLength) + sanitized = sanitized[..MaxOutgoingLength]; + + var pendingMessage = EnqueuePendingSelfMessage(descriptor, sanitized); + + try + { + await _apiController.SendChatMessage(new ChatSendRequestDto(descriptor, sanitized)).ConfigureAwait(false); + return true; + } + catch (Exception ex) + { + RemovePendingSelfMessage(pendingMessage); + Logger.LogWarning(ex, "Failed to send chat message"); + return false; + } + } + + public async Task SetParticipantMuteAsync(ChatChannelDescriptor descriptor, string token, bool mute) + { + if (string.IsNullOrWhiteSpace(token)) + return false; + + try + { + await _apiController.SetChatParticipantMute(new ChatParticipantMuteRequestDto(descriptor, token, mute)).ConfigureAwait(false); + return true; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to update chat participant mute state"); + return false; + } + } + + public Task ReportMessageAsync(ChatChannelDescriptor descriptor, string messageId, string reason, string? additionalContext) + { + if (string.IsNullOrWhiteSpace(messageId)) + { + return Task.FromResult(new ChatReportResult(false, "Unable to locate the selected message.")); + } + + var trimmedReason = reason?.Trim() ?? string.Empty; + if (trimmedReason.Length == 0) + { + return Task.FromResult(new ChatReportResult(false, "Please describe why you are reporting this message.")); + } + + using (_sync.EnterScope()) + { + if (!_chatEnabled) + { + return Task.FromResult(new ChatReportResult(false, "Enable chat before reporting messages.")); + } + + if (!_isConnected) + { + return Task.FromResult(new ChatReportResult(false, "Connect to the chat server before reporting messages.")); + } + } + + if (trimmedReason.Length > MaxReportReasonLength) + { + trimmedReason = trimmedReason[..MaxReportReasonLength]; + } + + string? context = null; + if (!string.IsNullOrWhiteSpace(additionalContext)) + { + context = additionalContext.Trim(); + if (context.Length > MaxReportContextLength) + { + context = context[..MaxReportContextLength]; + } + } + + var normalizedDescriptor = descriptor.WithNormalizedCustomKey(); + return ReportMessageInternalAsync(normalizedDescriptor, messageId.Trim(), trimmedReason, context); + } + + private async Task ReportMessageInternalAsync(ChatChannelDescriptor descriptor, string messageId, string reason, string? additionalContext) + { + try + { + await _apiController.ReportChatMessage(new ChatReportSubmitDto(descriptor, messageId, reason, additionalContext)).ConfigureAwait(false); + return new ChatReportResult(true, null); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to submit chat report"); + return new ChatReportResult(false, "Failed to submit report. Please try again."); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + Mediator.Subscribe(this, _ => HandleLogin()); + Mediator.Subscribe(this, _ => HandleLogout()); + Mediator.Subscribe(this, _ => ScheduleZonePresenceUpdate()); + Mediator.Subscribe(this, _ => ScheduleZonePresenceUpdate(force: true)); + Mediator.Subscribe(this, _ => HandleConnected()); + Mediator.Subscribe(this, _ => HandleConnected()); + Mediator.Subscribe(this, _ => HandleReconnecting()); + Mediator.Subscribe(this, _ => HandleReconnecting()); + Mediator.Subscribe(this, _ => RefreshGroupsFromPairManager()); + Mediator.Subscribe(this, _ => ScheduleZonePresenceUpdate(force: true)); + + if (_chatEnabled) + { + RegisterChatHandler(); + _ = RefreshChatChannelDefinitionsAsync(); + ScheduleZonePresenceUpdate(force: true); + _ = EnsureGroupPresenceAsync(force: true); + } + else + { + UpdateChannelsForDisabledState(); + PublishChannelListChanged(); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + UnregisterChatHandler(); + UnsubscribeAll(); + return Task.CompletedTask; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + UnregisterChatHandler(); + UnsubscribeAll(); + } + + base.Dispose(disposing); + } + + private void HandleLogin() + { + _isLoggedIn = true; + if (_chatEnabled) + { + ScheduleZonePresenceUpdate(force: true); + _ = EnsureGroupPresenceAsync(force: true); + } + } + + private void HandleLogout() + { + _isLoggedIn = false; + if (_chatEnabled) + { + ScheduleZonePresenceUpdate(force: true); + } + } + + private void HandleConnected() + { + _isConnected = true; + + using (_sync.EnterScope()) + { + _selfTokens.Clear(); + _pendingSelfMessages.Clear(); + + foreach (var state in _channels.Values) + { + state.IsConnected = _chatEnabled; + if (_chatEnabled && state.Type == ChatChannelType.Group) + { + state.IsAvailable = true; + state.StatusText = null; + } + else if (!_chatEnabled) + { + state.IsAvailable = false; + state.StatusText = "Chat services disabled"; + } + } + } + + PublishChannelListChanged(); + + if (_chatEnabled) + { + _ = RefreshChatChannelDefinitionsAsync(); + ScheduleZonePresenceUpdate(force: true); + _ = EnsureGroupPresenceAsync(force: true); + } + } + + private void HandleReconnecting() + { + _isConnected = false; + + using (_sync.EnterScope()) + { + _selfTokens.Clear(); + _pendingSelfMessages.Clear(); + foreach (var state in _channels.Values) + { + state.IsConnected = false; + if (_chatEnabled) + { + state.StatusText = "Disconnected from chat server"; + if (state.Type == ChatChannelType.Group) + { + state.IsAvailable = false; + } + } + else + { + state.StatusText = "Chat services disabled"; + state.IsAvailable = false; + } + } + } + + PublishChannelListChanged(); + } + + private async Task RefreshChatChannelDefinitionsAsync() + { + if (!_chatEnabled) + return; + + try + { + var zones = await _apiController.GetZoneChatChannelsAsync().ConfigureAwait(false); + var groups = await _apiController.GetGroupChatChannelsAsync().ConfigureAwait(false); + + ApplyZoneDefinitions(zones); + ApplyGroupDefinitions(groups); + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to refresh chat channel definitions"); + } + } + + private void RegisterChatHandler() + { + if (_chatHandlerRegistered) + return; + + _apiController.RegisterChatMessageHandler(OnChatMessageReceived); + _chatHandlerRegistered = true; + } + + private void UnregisterChatHandler() + { + if (!_chatHandlerRegistered) + return; + + _apiController.UnregisterChatMessageHandler(OnChatMessageReceived); + _chatHandlerRegistered = false; + } + + private void UpdateChannelsForDisabledState() + { + using (_sync.EnterScope()) + { + foreach (var state in _channels.Values) + { + state.IsConnected = false; + state.IsAvailable = false; + state.StatusText = "Chat services disabled"; + } + } + } + + private void ScheduleZonePresenceUpdate(bool force = false) + { + if (!_chatEnabled) + return; + + _ = UpdateZonePresenceAsync(force); + } + + private async Task UpdateZonePresenceAsync(bool force = false) + { + if (!_chatEnabled) + return; + + if (!_isLoggedIn || !_apiController.IsConnected) + { + await LeaveCurrentZoneAsync(force, 0, 0).ConfigureAwait(false); + return; + } + + try + { + var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false); + var territoryId = (ushort)location.TerritoryId; + var worldId = (ushort)location.ServerId; + + string? zoneKey; + ZoneChannelDefinition? definition = null; + + using (_sync.EnterScope()) + { + _territoryToZoneKey.TryGetValue(territoryId, out zoneKey); + if (zoneKey is not null) + { + _zoneDefinitions.TryGetValue(zoneKey, out var def); + definition = def; + } + } + + if (definition is null) + { + await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false); + return; + } + + var descriptor = await BuildZoneDescriptorAsync(definition.Value).ConfigureAwait(false); + if (descriptor is null) + { + await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false); + return; + } + + bool shouldForceSend; + ChatMessageEntry? zoneSeparatorEntry = null; + + using (_sync.EnterScope()) + { + var state = EnsureZoneStateLocked(); + state.DisplayName = definition.Value.DisplayName; + state.Descriptor = descriptor.Value; + state.IsConnected = _chatEnabled && _isConnected; + state.IsAvailable = _chatEnabled; + state.StatusText = _chatEnabled ? null : "Chat services disabled"; + + var previousDescriptor = _lastZoneDescriptor; + var zoneChanged = previousDescriptor.HasValue && !ChannelDescriptorsMatch(previousDescriptor.Value, descriptor.Value); + + _activeChannelKey = ZoneChannelKey; + shouldForceSend = force || !previousDescriptor.HasValue || zoneChanged; + if (zoneChanged && state.Messages.Any(m => !m.IsSystem)) + { + zoneSeparatorEntry = AddZoneSeparatorLocked(state, definition.Value.DisplayName); + } + + _lastZoneDescriptor = descriptor; + } + + if (zoneSeparatorEntry is not null) + { + Mediator.Publish(new ChatChannelMessageAdded(ZoneChannelKey, zoneSeparatorEntry)); + } + + PublishChannelListChanged(); + await SendPresenceAsync(descriptor.Value, territoryId, isActive: true, force: shouldForceSend).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to update zone chat presence"); + } + } + + private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId, ushort worldId) + { + ChatChannelDescriptor? descriptor = null; + + using (_sync.EnterScope()) + { + descriptor = _lastZoneDescriptor; + _lastZoneDescriptor = null; + + if (_channels.TryGetValue(ZoneChannelKey, out var state)) + { + state.IsConnected = _isConnected; + state.IsAvailable = false; + state.StatusText = !_chatEnabled + ? "Chat services disabled" + : (_isConnected ? ZoneUnavailableMessage : "Disconnected from chat server"); + if (territoryId != 0 + && _dalamudUtilService.TerritoryData.Value.TryGetValue(territoryId, out var territoryName) + && !string.IsNullOrWhiteSpace(territoryName)) + { + state.DisplayName = territoryName; + } + else + { + state.DisplayName = "Zone Chat"; + } + + if (worldId != 0) + { + state.Descriptor = new ChatChannelDescriptor + { + Type = ChatChannelType.Zone, + WorldId = worldId, + ZoneId = territoryId, + CustomKey = string.Empty + }; + } + } + + if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal)) + { + _activeChannelKey = _channelOrder.FirstOrDefault(key => !string.Equals(key, ZoneChannelKey, StringComparison.Ordinal)); + } + } + + PublishChannelListChanged(); + + if (descriptor.HasValue) + { + await SendPresenceAsync(descriptor.Value, territoryId, isActive: false, force: force).ConfigureAwait(false); + } + } + + private async Task BuildZoneDescriptorAsync(ZoneChannelDefinition definition) + { + try + { + var worldId = (ushort)await _dalamudUtilService.GetWorldIdAsync().ConfigureAwait(false); + return definition.Descriptor with { WorldId = worldId }; + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to obtain world id for zone chat"); + return null; + } + } + + private void ApplyZoneDefinitions(IReadOnlyList? infos) + { + var infoList = infos ?? Array.Empty(); + + using (_sync.EnterScope()) + { + _zoneDefinitions.Clear(); + _territoryToZoneKey.Clear(); + + foreach (var info in infoList) + { + var descriptor = info.Channel.WithNormalizedCustomKey(); + var key = descriptor.CustomKey ?? string.Empty; + if (string.IsNullOrWhiteSpace(key)) + continue; + + var territories = info.Territories? + .SelectMany(EnumerateTerritoryKeys) + .Where(n => n.Length > 0) + .ToHashSet(StringComparer.OrdinalIgnoreCase) + ?? new HashSet(StringComparer.OrdinalIgnoreCase); + + _zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories); + } + + var territoryData = _dalamudUtilService.TerritoryData.Value; + foreach (var kvp in territoryData) + { + foreach (var variant in EnumerateTerritoryKeys(kvp.Value)) + { + foreach (var def in _zoneDefinitions.Values) + { + if (def.TerritoryNames.Contains(variant)) + { + _territoryToZoneKey[kvp.Key] = def.Key; + break; + } + } + } + } + + if (_zoneDefinitions.Count == 0) + { + RemoveZoneStateLocked(); + } + else + { + var state = EnsureZoneStateLocked(); + state.DisplayName = "Zone Chat"; + state.IsConnected = _chatEnabled && _isConnected; + state.IsAvailable = false; + state.StatusText = _chatEnabled ? ZoneUnavailableMessage : "Chat services disabled"; + } + + UpdateChannelOrderLocked(); + } + + PublishChannelListChanged(); + } + + private void ApplyGroupDefinitions(IReadOnlyList? infos) + { + var infoList = infos ?? Array.Empty(); + var descriptorsToJoin = new List(); + var descriptorsToLeave = new List(); + + using (_sync.EnterScope()) + { + var remainingGroups = new HashSet(_groupDefinitions.Keys, StringComparer.OrdinalIgnoreCase); + + foreach (var info in infoList) + { + var descriptor = info.Channel.WithNormalizedCustomKey(); + var groupId = info.GroupId; + if (string.IsNullOrWhiteSpace(groupId)) + continue; + + remainingGroups.Remove(groupId); + + _groupDefinitions[groupId] = new GroupChannelDefinition(groupId, info.DisplayName ?? groupId, descriptor, info.IsOwner); + + var key = BuildChannelKey(descriptor); + if (!_channels.TryGetValue(key, out var state)) + { + state = new ChatChannelState(key, ChatChannelType.Group, info.DisplayName ?? groupId, descriptor); + state.IsConnected = _chatEnabled && _isConnected; + state.IsAvailable = _chatEnabled && _isConnected; + state.StatusText = !_chatEnabled + ? "Chat services disabled" + : (_isConnected ? null : "Disconnected from chat server"); + _channels[key] = state; + _lastReadCounts[key] = 0; + if (_chatEnabled) + { + descriptorsToJoin.Add(descriptor); + } + } + else + { + state.DisplayName = info.DisplayName ?? groupId; + state.Descriptor = descriptor; + state.IsConnected = _chatEnabled && _isConnected; + state.IsAvailable = _chatEnabled && _isConnected; + state.StatusText = !_chatEnabled + ? "Chat services disabled" + : (_isConnected ? null : "Disconnected from chat server"); + } + } + + foreach (var removedGroupId in remainingGroups) + { + if (_groupDefinitions.TryGetValue(removedGroupId, out var definition)) + { + var key = BuildChannelKey(definition.Descriptor); + if (_channels.TryGetValue(key, out var state)) + { + descriptorsToLeave.Add(state.Descriptor); + _channels.Remove(key); + _lastReadCounts.Remove(key); + _lastPresenceStates.Remove(BuildPresenceKey(state.Descriptor)); + _selfTokens.Remove(key); + _pendingSelfMessages.RemoveAll(p => string.Equals(p.ChannelKey, key, StringComparison.Ordinal)); + if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal)) + { + _activeChannelKey = null; + } + } + + _groupDefinitions.Remove(removedGroupId); + } + } + + UpdateChannelOrderLocked(); + } + + foreach (var descriptor in descriptorsToLeave) + { + _ = SendPresenceAsync(descriptor, 0, isActive: false, force: true); + } + + foreach (var descriptor in descriptorsToJoin) + { + _ = SendPresenceAsync(descriptor, 0, isActive: true, force: true); + } + + PublishChannelListChanged(); + } + + private void RefreshGroupsFromPairManager() + { + var snapshot = _pairUiService.GetSnapshot(); + var groups = snapshot.Groups.ToList(); + if (groups.Count == 0) + { + ApplyGroupDefinitions(Array.Empty()); + return; + } + + var infos = new List(groups.Count); + foreach (var group in groups) + { + var descriptor = new ChatChannelDescriptor + { + Type = ChatChannelType.Group, + WorldId = 0, + ZoneId = 0, + CustomKey = group.Group.GID + }; + + var displayName = string.IsNullOrWhiteSpace(group.Group.Alias) ? group.Group.GID : group.Group.Alias; + var isOwner = string.Equals(group.Owner.UID, _apiController.UID, StringComparison.Ordinal); + + infos.Add(new GroupChatChannelInfoDto(descriptor, displayName, group.Group.GID, isOwner)); + } + + ApplyGroupDefinitions(infos); + } + + private async Task EnsureGroupPresenceAsync(bool force = false) + { + if (!_chatEnabled) + return; + + List descriptors; + using (_sync.EnterScope()) + { + descriptors = _channels.Values + .Where(state => state.Type == ChatChannelType.Group) + .Select(state => state.Descriptor) + .ToList(); + } + + foreach (var descriptor in descriptors) + { + await SendPresenceAsync(descriptor, 0, isActive: true, force: force).ConfigureAwait(false); + } + } + + private async Task SendPresenceAsync(ChatChannelDescriptor descriptor, ushort territoryId, bool isActive, bool force) + { + if (!_apiController.IsConnected) + return; + + if (!_chatEnabled && isActive) + return; + + var presenceKey = BuildPresenceKey(descriptor); + bool stateMatches; + + using (_sync.EnterScope()) + { + stateMatches = !force + && _lastPresenceStates.TryGetValue(presenceKey, out var lastState) + && lastState == isActive; + } + + if (stateMatches) + return; + + try + { + await _apiController.UpdateChatPresence(new ChatPresenceUpdateDto(descriptor, territoryId, isActive)).ConfigureAwait(false); + + using (_sync.EnterScope()) + { + if (isActive) + { + _lastPresenceStates[presenceKey] = true; + } + else + { + _lastPresenceStates.Remove(presenceKey); + } + } + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to update chat presence"); + } + } + + private PendingSelfMessage EnqueuePendingSelfMessage(ChatChannelDescriptor descriptor, string message) + { + var normalized = descriptor.WithNormalizedCustomKey(); + var key = normalized.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(normalized); + var pending = new PendingSelfMessage(key, message); + + using (_sync.EnterScope()) + { + _pendingSelfMessages.Add(pending); + while (_pendingSelfMessages.Count > 20) + { + _pendingSelfMessages.RemoveAt(0); + } + } + + return pending; + } + + private void RemovePendingSelfMessage(PendingSelfMessage pending) + { + using (_sync.EnterScope()) + { + var index = _pendingSelfMessages.FindIndex(p => + string.Equals(p.ChannelKey, pending.ChannelKey, StringComparison.Ordinal) && + string.Equals(p.Message, pending.Message, StringComparison.Ordinal)); + + if (index >= 0) + { + _pendingSelfMessages.RemoveAt(index); + } + } + } + + private void OnChatMessageReceived(ChatMessageDto dto) + { + var descriptor = dto.Channel.WithNormalizedCustomKey(); + var key = descriptor.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(descriptor); + var fromSelf = IsMessageFromSelf(dto, key); + var message = BuildMessage(dto, fromSelf); + bool publishChannelList = false; + + using (_sync.EnterScope()) + { + if (!_channels.TryGetValue(key, out var state)) + { + var displayName = descriptor.Type switch + { + ChatChannelType.Zone => _zoneDefinitions.TryGetValue(descriptor.CustomKey ?? string.Empty, out var def) + ? def.DisplayName + : "Zone Chat", + ChatChannelType.Group => descriptor.CustomKey ?? "Syncshell", + _ => descriptor.CustomKey ?? "Chat" + }; + + state = new ChatChannelState( + key, + descriptor.Type, + displayName, + descriptor.Type == ChatChannelType.Zone ? (_lastZoneDescriptor ?? descriptor) : descriptor); + + state.IsConnected = _isConnected; + state.IsAvailable = descriptor.Type == ChatChannelType.Group && _isConnected; + state.StatusText = descriptor.Type == ChatChannelType.Zone ? ZoneUnavailableMessage : (_isConnected ? null : "Disconnected from chat server"); + + _channels[key] = state; + _lastReadCounts[key] = 0; + publishChannelList = true; + } + + state.Descriptor = descriptor.Type == ChatChannelType.Zone ? (_lastZoneDescriptor ?? descriptor) : descriptor; + state.Messages.Add(message); + if (state.Messages.Count > MaxMessageHistory) + { + state.Messages.RemoveAt(0); + } + + if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal)) + { + state.HasUnread = false; + state.UnreadCount = 0; + _lastReadCounts[key] = state.Messages.Count; + } + else + { + var lastRead = _lastReadCounts.TryGetValue(key, out var readCount) ? readCount : 0; + var unreadFromHistory = Math.Max(0, state.Messages.Count - lastRead); + var incrementalUnread = Math.Min(state.UnreadCount + 1, MaxUnreadCount); + state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount); + state.HasUnread = state.UnreadCount > 0; + } + } + + Mediator.Publish(new ChatChannelMessageAdded(key, message)); + + if (publishChannelList) + { + using (_sync.EnterScope()) + { + UpdateChannelOrderLocked(); + } + + PublishChannelListChanged(); + } + } + + private bool IsMessageFromSelf(ChatMessageDto dto, string channelKey) + { + if (dto.Sender.User?.UID is { } uid && string.Equals(uid, _apiController.UID, StringComparison.Ordinal)) + { + using (_sync.EnterScope()) + { + _selfTokens[channelKey] = dto.Sender.Token; + } + + return true; + } + + using (_sync.EnterScope()) + { + if (_selfTokens.TryGetValue(channelKey, out var token) && + string.Equals(token, dto.Sender.Token, StringComparison.Ordinal)) + { + return true; + } + + var index = _pendingSelfMessages.FindIndex(p => + string.Equals(p.ChannelKey, channelKey, StringComparison.Ordinal) && + string.Equals(p.Message, dto.Message, StringComparison.Ordinal)); + + if (index >= 0) + { + _pendingSelfMessages.RemoveAt(index); + _selfTokens[channelKey] = dto.Sender.Token; + return true; + } + } + + return false; + } + + private ChatMessageEntry BuildMessage(ChatMessageDto dto, bool fromSelf) + { + var displayName = ResolveDisplayName(dto, fromSelf); + return new ChatMessageEntry(dto, displayName, fromSelf, DateTime.UtcNow); + } + + private ChatMessageEntry AddZoneSeparatorLocked(ChatChannelState state, string zoneDisplayName) + { + var separator = new ChatMessageEntry( + null, + string.Empty, + false, + DateTime.UtcNow, + new ChatSystemEntry(ChatSystemEntryType.ZoneSeparator, zoneDisplayName)); + + state.Messages.Add(separator); + if (state.Messages.Count > MaxMessageHistory) + { + state.Messages.RemoveAt(0); + } + + if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal)) + { + state.HasUnread = false; + state.UnreadCount = 0; + _lastReadCounts[ZoneChannelKey] = state.Messages.Count; + } + else if (_lastReadCounts.TryGetValue(ZoneChannelKey, out var readCount)) + { + _lastReadCounts[ZoneChannelKey] = readCount + 1; + } + else + { + _lastReadCounts[ZoneChannelKey] = state.Messages.Count; + } + + return separator; + } + + private string ResolveDisplayName(ChatMessageDto dto, bool fromSelf) + { + var isZone = dto.Channel.Type == ChatChannelType.Zone; + if (!string.IsNullOrEmpty(dto.Sender.HashedCid) && + _actorObjectService.TryGetValidatedActorByHash(dto.Sender.HashedCid, out var descriptor) && + !string.IsNullOrWhiteSpace(descriptor.Name)) + { + return descriptor.Name; + } + + if (fromSelf && isZone && dto.Sender.CanResolveProfile) + { + try + { + return _dalamudUtilService.GetPlayerNameAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to resolve self name for chat message"); + } + } + + if (dto.Sender.Kind == ChatSenderKind.IdentifiedUser && dto.Sender.User is not null) + { + return dto.Sender.User.AliasOrUID; + } + + if (!string.IsNullOrWhiteSpace(dto.Sender.DisplayName)) + { + return dto.Sender.DisplayName!; + } + + return dto.Sender.Token; + } + + private void UpdateChannelOrderLocked() + { + _channelOrder.Clear(); + + var configuredOrder = _chatConfigService.Current.ChannelOrder; + if (configuredOrder.Count > 0) + { + var seen = new HashSet(StringComparer.Ordinal); + foreach (var key in configuredOrder) + { + if (_channels.ContainsKey(key) && seen.Add(key)) + { + _channelOrder.Add(key); + } + } + + var remaining = _channels.Values + .Where(state => !seen.Contains(state.Key)) + .ToList(); + + if (remaining.Count > 0) + { + var zoneKeys = remaining + .Where(state => state.Type == ChatChannelType.Zone) + .Select(state => state.Key); + var groupKeys = remaining + .Where(state => state.Type == ChatChannelType.Group) + .OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase) + .Select(state => state.Key); + + _channelOrder.AddRange(zoneKeys); + _channelOrder.AddRange(groupKeys); + } + } + else + { + if (_channels.ContainsKey(ZoneChannelKey)) + { + _channelOrder.Add(ZoneChannelKey); + } + + var groups = _channels.Values + .Where(state => state.Type == ChatChannelType.Group) + .OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase) + .Select(state => state.Key); + + _channelOrder.AddRange(groups); + } + + if (_activeChannelKey is null && _channelOrder.Count > 0) + { + _activeChannelKey = _channelOrder[0]; + } + else if (_activeChannelKey is not null && !_channelOrder.Contains(_activeChannelKey, StringComparer.Ordinal)) + { + _activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null; + } + } + + private void PublishChannelListChanged() => Mediator.Publish(new ChatChannelsUpdated()); + + private static IEnumerable EnumerateTerritoryKeys(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + yield break; + + var normalizedFull = NormalizeKey(value); + if (normalizedFull.Length > 0) + yield return normalizedFull; + + var segments = value.Split('-', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (segments.Length <= 1) + yield break; + + for (var i = 1; i < segments.Length; i++) + { + var composite = string.Join(" - ", segments[i..]); + var normalized = NormalizeKey(composite); + if (normalized.Length > 0) + yield return normalized; + } + } + + private static string NormalizeKey(string? value) + => string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToUpperInvariant(); + + private static string BuildChannelKey(ChatChannelDescriptor descriptor) + => $"{(int)descriptor.Type}:{NormalizeKey(descriptor.CustomKey)}"; + + private static string BuildPresenceKey(ChatChannelDescriptor descriptor) + => $"{(int)descriptor.Type}:{descriptor.WorldId}:{NormalizeKey(descriptor.CustomKey)}"; + + private static bool ChannelDescriptorsMatch(ChatChannelDescriptor left, ChatChannelDescriptor right) + => left.Type == right.Type + && string.Equals(NormalizeKey(left.CustomKey), NormalizeKey(right.CustomKey), StringComparison.Ordinal) + && left.WorldId == right.WorldId; + + private ChatChannelState EnsureZoneStateLocked() + { + if (!_channels.TryGetValue(ZoneChannelKey, out var state)) + { + state = new ChatChannelState(ZoneChannelKey, ChatChannelType.Zone, "Zone Chat", new ChatChannelDescriptor { Type = ChatChannelType.Zone }); + state.IsConnected = _chatEnabled && _isConnected; + state.IsAvailable = false; + state.StatusText = _chatEnabled ? ZoneUnavailableMessage : "Chat services disabled"; + _channels[ZoneChannelKey] = state; + _lastReadCounts[ZoneChannelKey] = 0; + UpdateChannelOrderLocked(); + } + + return state; + } + + private void RemoveZoneStateLocked() + { + if (_channels.Remove(ZoneChannelKey)) + { + _lastReadCounts.Remove(ZoneChannelKey); + _lastPresenceStates.Remove(BuildPresenceKey(new ChatChannelDescriptor { Type = ChatChannelType.Zone })); + _selfTokens.Remove(ZoneChannelKey); + _pendingSelfMessages.RemoveAll(p => string.Equals(p.ChannelKey, ZoneChannelKey, StringComparison.Ordinal)); + if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal)) + { + _activeChannelKey = null; + } + UpdateChannelOrderLocked(); + } + } + + private sealed class ChatChannelState + { + public ChatChannelState(string key, ChatChannelType type, string displayName, ChatChannelDescriptor descriptor) + { + Key = key; + Type = type; + DisplayName = displayName; + Descriptor = descriptor; + Messages = new List(); + } + + public string Key { get; } + public ChatChannelType Type { get; } + public string DisplayName { get; set; } + public ChatChannelDescriptor Descriptor { get; set; } + public bool IsConnected { get; set; } + public bool IsAvailable { get; set; } + public string? StatusText { get; set; } + public bool HasUnread { get; set; } + public int UnreadCount { get; set; } + public List Messages { get; } + } + + private readonly record struct ZoneChannelDefinition( + string Key, + string DisplayName, + ChatChannelDescriptor Descriptor, + HashSet TerritoryNames); + + private readonly record struct GroupChannelDefinition( + string GroupId, + string DisplayName, + ChatChannelDescriptor Descriptor, + bool IsOwner); + + private readonly record struct PendingSelfMessage(string ChannelKey, string Message); +} diff --git a/LightlessSync/Services/CommandManagerService.cs b/LightlessSync/Services/CommandManagerService.cs index 88f8780..0014b3a 100644 --- a/LightlessSync/Services/CommandManagerService.cs +++ b/LightlessSync/Services/CommandManagerService.cs @@ -48,7 +48,8 @@ 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 finder - Opens the Lightfinder window" + Environment.NewLine + + "\t /light chat - Opens the Lightless Chat window" }); } @@ -131,7 +132,11 @@ public sealed class CommandManagerService : IDisposable } else if (string.Equals(splitArgs[0], "finder", StringComparison.OrdinalIgnoreCase)) { - _mediator.Publish(new UiToggleMessage(typeof(BroadcastUI))); + _mediator.Publish(new UiToggleMessage(typeof(LightFinderUI))); + } + else if (string.Equals(splitArgs[0], "chat", StringComparison.OrdinalIgnoreCase)) + { + _mediator.Publish(new UiToggleMessage(typeof(ZoneChatUi))); } } } \ No newline at end of file diff --git a/LightlessSync/Services/Compactor/BatchFileFragService.cs b/LightlessSync/Services/Compactor/BatchFileFragService.cs index b31919e..b99934b 100644 --- a/LightlessSync/Services/Compactor/BatchFileFragService.cs +++ b/LightlessSync/Services/Compactor/BatchFileFragService.cs @@ -92,13 +92,13 @@ namespace LightlessSync.Services.Compactor } if ((flushAt - DateTime.UtcNow) <= TimeSpan.Zero) break; - try - { - await Task.Delay(TimeSpan.FromMilliseconds(5), _cts.Token).ConfigureAwait(false); + try + { + await Task.Delay(TimeSpan.FromMilliseconds(5), _cts.Token).ConfigureAwait(false); } - catch - { - break; + catch + { + break; } } @@ -124,8 +124,8 @@ namespace LightlessSync.Services.Compactor } } } - catch (OperationCanceledException) - { + catch (OperationCanceledException) + { //Shutting down worker, exception called } } @@ -145,17 +145,13 @@ namespace LightlessSync.Services.Compactor if (_useShell) { - var inner = "filefrag -v " + string.Join(' ', list.Select(QuoteSingle)); + var inner = "filefrag -v -- " + string.Join(' ', list.Select(QuoteSingle)); res = _runShell(inner, timeoutMs: 15000, workingDir: "/"); } else { - var args = new List { "-v" }; - foreach (var path in list) - { - args.Add(' ' + path); - } - + var args = new List { "-v", "--" }; + args.AddRange(list); res = _runDirect("filefrag", args, workingDir: "/", timeoutMs: 15000); } @@ -200,7 +196,7 @@ namespace LightlessSync.Services.Compactor /// Regex of the File Size return on the Linux/Wine systems, giving back the amount /// /// Regex of the File Size - [GeneratedRegex(@"^File size of (/.+?) is ", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant,matchTimeoutMilliseconds: 500)] + [GeneratedRegex(@"^File size of (/.+?) is ", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 500)] private static partial Regex SizeRegex(); /// diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index 464fee1..53bbb45 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -1,14 +1,18 @@ -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.PlayerData.Pairs; +using LightlessSync.LightlessConfiguration.Models; +using LightlessSync.Services.Mediator; 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; @@ -20,11 +24,17 @@ internal class ContextMenuService : IHostedService private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtil; private readonly IClientState _clientState; - private readonly PairManager _pairManager; + private readonly PairUiService _pairUiService; 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, @@ -33,11 +43,15 @@ internal class ContextMenuService : IHostedService ILogger logger, DalamudUtilService dalamudUtil, ApiController apiController, - IObjectTable objectTable, + IObjectTable objectTable, LightlessConfigService configService, PairRequestService pairRequestService, - PairManager pairManager, - IClientState clientState) + PairUiService pairUiService, + IClientState clientState, + LightFinderScannerService broadcastScannerService, + LightFinderService broadcastService, + LightlessProfileManager lightlessProfileManager, + LightlessMediator mediator) { _contextMenu = contextMenu; _pluginInterface = pluginInterface; @@ -47,9 +61,13 @@ internal class ContextMenuService : IHostedService _apiController = apiController; _objectTable = objectTable; _configService = configService; - _pairManager = pairManager; + _pairUiService = pairUiService; _pairRequestService = pairRequestService; _clientState = clientState; + _broadcastScannerService = broadcastScannerService; + _broadcastService = broadcastService; + _lightlessProfileManager = lightlessProfileManager; + _mediator = mediator; } public Task StartAsync(CancellationToken cancellationToken) @@ -78,52 +96,109 @@ 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) + 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); 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); + } + + 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) || _clientState.LocalPlayer.GameObjectId == target.TargetObjectId) + 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)) - return; - - if (!_configService.Current.EnableRightClickMenus) - return; - - args.AddMenuItem(new MenuItem { - Name = "Send Direct Pair Request", - PrefixChar = 'L', - UseDefaultPrefix = false, - PrefixColor = 708, - OnClicked = async _ => await HandleSelection(args).ConfigureAwait(false) - }); + _logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId); + return; + } + + string? targetHashedCid = null; + if (_broadcastService.IsBroadcasting) + { + 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); } + private void AddDirectPairMenuItem(IMenuOpenedArgs args) + { + UiSharedService.AddContextMenuItem( + args, + name: "Send Direct Pair Request", + prefixChar: 'L', + colorMenuItem: _lightlessPrefixColor, + onClick: () => HandleSelection(args)); + } + + private HashSet 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) @@ -159,9 +234,48 @@ internal class ContextMenuService : IHostedService } } - private HashSet VisibleUserIds => [.. _pairManager.DirectPairs - .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) - .Select(u => (ulong)u.PlayerCharacterId)]; + 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 IPlayerCharacter? GetPlayerFromObjectTable(MenuTargetDefault target) { @@ -200,8 +314,6 @@ 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(); diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index e5fd735..c8668eb 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -12,7 +12,10 @@ 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; @@ -21,7 +24,9 @@ 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; @@ -37,23 +42,27 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber private readonly IGameGui _gameGui; private readonly ILogger _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; + 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 readonly Dictionary _playerCharas = new(StringComparer.Ordinal); - private readonly List _notUpdatedCharas = []; + private ushort _lastWorldId = 0; private bool _sentBetweenAreas = false; private Lazy _cid; public DalamudUtilService(ILogger logger, IClientState clientState, IObjectTable objectTable, IFramework framework, IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig, - BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector, - LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService) + ActorObjectService actorObjectService, BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector, + LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService, Lazy pairFactory) { _logger = logger; _clientState = clientState; @@ -63,11 +72,14 @@ 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(Dalamud.Game.ClientLanguage.English)! @@ -119,17 +131,24 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber mediator.Subscribe(this, (msg) => { if (clientState.IsPvP) 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; + if (!ResolvePairAddress(msg.Pair, out var pair, out var addr)) return; var useFocusTarget = _configService.Current.UseFocusTarget; _ = RunOnFrameworkThread(() => { + var gameObject = CreateGameObject(addr); + if (gameObject is null) return; if (useFocusTarget) - targetManager.FocusTarget = CreateGameObject(addr); + { + _targetManager.FocusTarget = gameObject; + if (_FocusPairIdent.HasValue && _FocusPairIdent.Value.Equals(pair.UniqueIdent)) + { + _FocusOriginalTarget = _targetManager.FocusTarget; + } + } else - targetManager.Target = CreateGameObject(addr); + { + _targetManager.Target = gameObject; + } }).ConfigureAwait(false); }); IsWine = Util.IsWine(); @@ -139,6 +158,61 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber private Lazy 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 { @@ -165,6 +239,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool IsInCombat { get; private set; } = false; public bool IsPerforming { get; private set; } = false; public bool IsInInstance { get; private set; } = false; + public bool IsInDuty => _condition[ConditionFlag.BoundByDuty]; public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles; public uint ClassJobId => _classJobId!.Value; public Lazy> JobData { get; private set; } @@ -174,6 +249,32 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool IsLodEnabled { get; private set; } public LightlessMediator Mediator { get; } + public bool IsInFieldOperation + { + get + { + if (!IsInDuty) + { + return false; + } + + var territoryId = _clientState.TerritoryType; + if (territoryId == 0) + { + return false; + } + + if (!TerritoryData.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name)) + { + return false; + } + + return name.Contains("Eureka", StringComparison.OrdinalIgnoreCase) + || name.Contains("Bozja", StringComparison.OrdinalIgnoreCase) + || name.Contains("Zadnor", StringComparison.OrdinalIgnoreCase); + } + } + public IGameObject? CreateGameObject(IntPtr reference) { EnsureIsOnFramework(); @@ -194,7 +295,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber { EnsureIsOnFramework(); var objTableObj = _objectTable[index]; - if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null; + if (objTableObj!.ObjectKind != DalamudObjectKind.Player) return null; return (ICharacter)objTableObj; } @@ -226,13 +327,19 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public IEnumerable GetGposeCharactersFromObjectTable() { - return _objectTable.Where(o => o.ObjectIndex > 200 && o.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player).Cast(); + 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; + } } public bool GetIsPlayerPresent() { EnsureIsOnFramework(); - return _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid(); + return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid(); } public async Task GetIsPlayerPresentAsync() @@ -245,7 +352,28 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber EnsureIsOnFramework(); playerPointer ??= GetPlayerPtr(); if (playerPointer == IntPtr.Zero) return IntPtr.Zero; - return _objectTable.GetObjectAddress(((GameObject*)playerPointer)->ObjectIndex + 1); + + 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); } public async Task GetMinionOrMountAsync(IntPtr? playerPointer = null) @@ -268,6 +396,62 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber return await RunOnFrameworkThread(() => GetPetPtr(playerPointer)).ConfigureAwait(false); } + private unsafe nint FindOwnedObject(uint ownerEntityId, nint ownerAddress, Func 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 GetPlayerCharacterAsync() { return await RunOnFrameworkThread(GetPlayerCharacter).ConfigureAwait(false); @@ -276,19 +460,20 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public IPlayerCharacter GetPlayerCharacter() { EnsureIsOnFramework(); - return _clientState.LocalPlayer!; + return _objectTable.LocalPlayer!; } public IntPtr GetPlayerCharacterFromCachedTableByIdent(string characterName) { - if (_playerCharas.TryGetValue(characterName, out var pchar)) return pchar.Address; + if (_actorObjectService.TryGetValidatedActorByHash(characterName, out var actor)) + return actor.Address; return IntPtr.Zero; } public string GetPlayerName() { EnsureIsOnFramework(); - return _clientState.LocalPlayer?.Name.ToString() ?? "--"; + return _objectTable.LocalPlayer?.Name.ToString() ?? "--"; } public async Task GetPlayerNameAsync() @@ -313,6 +498,24 @@ 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(); @@ -321,7 +524,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public IntPtr GetPlayerPtr() { EnsureIsOnFramework(); - return _clientState.LocalPlayer?.Address ?? IntPtr.Zero; + return _objectTable.LocalPlayer?.Address ?? IntPtr.Zero; } public async Task GetPlayerPointerAsync() @@ -332,13 +535,13 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public uint GetHomeWorldId() { EnsureIsOnFramework(); - return _clientState.LocalPlayer?.HomeWorld.RowId ?? 0; + return _objectTable.LocalPlayer?.HomeWorld.RowId ?? 0; } public uint GetWorldId() { EnsureIsOnFramework(); - return _clientState.LocalPlayer!.CurrentWorld.RowId; + return _objectTable.LocalPlayer!.CurrentWorld.RowId; } public unsafe LocationInfo GetMapData() @@ -347,8 +550,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber var agentMap = AgentMap.Instance(); var houseMan = HousingManager.Instance(); uint serverId = 0; - if (_clientState.LocalPlayer == null) serverId = 0; - else serverId = _clientState.LocalPlayer.CurrentWorld.RowId; + if (_objectTable.LocalPlayer == null) serverId = 0; + else serverId = _objectTable.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()); @@ -436,17 +639,13 @@ 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) { - 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); - } - } - else act(); + return; + } + + await _framework.RunOnFrameworkThread(act).ConfigureAwait(false); }).ConfigureAwait(false); } @@ -455,18 +654,12 @@ 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) { - 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 func.Invoke(); } - return func.Invoke(); + return await _framework.RunOnFrameworkThread(func).ConfigureAwait(false); }).ConfigureAwait(false); } @@ -476,7 +669,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber _framework.Update += FrameworkOnUpdate; if (IsLoggedIn) { - _classJobId = _clientState.LocalPlayer!.ClassJob.RowId; + _classJobId = _objectTable.LocalPlayer!.ClassJob.RowId; } _logger.LogInformation("Started DalamudUtilService"); @@ -489,6 +682,17 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber Mediator.UnsubscribeAll(this); _framework.Update -= FrameworkOnUpdate; + if (_FocusPairIdent.HasValue) + { + if (_framework.IsInFrameworkUpdateThread) + { + ReleaseFocusUnsafe(); + } + else + { + _ = RunOnFrameworkThread(ReleaseFocusUnsafe); + } + } return Task.CompletedTask; } @@ -513,15 +717,11 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber { logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler); curWaitTime += tick; - await Task.Delay(tick).ConfigureAwait(true); + await Task.Delay(tick, ct.Value).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); @@ -535,7 +735,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber const int tick = 250; int curWaitTime = 0; _logger.LogTrace("RenderFlags: {flags}", obj->RenderFlags.ToString("X")); - while (obj->RenderFlags != 0x00 && curWaitTime < timeOut) + while (obj->RenderFlags != VisibilityFlags.None && curWaitTime < timeOut) { _logger.LogTrace($"Waiting for gpose actor to finish drawing"); curWaitTime += tick; @@ -552,8 +752,12 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber internal (string Name, nint Address) FindPlayerByNameHash(string ident) { - _playerCharas.TryGetValue(ident, out var result); - return result; + if (_actorObjectService.TryGetValidatedActorByHash(ident, out var descriptor)) + { + return (descriptor.Name, descriptor.Address); + } + + return default; } public string? GetWorldNameFromPlayerAddress(nint address) @@ -576,7 +780,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber bool isDrawingChanged = false; if ((nint)drawObj != IntPtr.Zero) { - isDrawing = gameObj->RenderFlags == 0b100000000000; + isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None; if (!isDrawing) { isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0; @@ -629,7 +833,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber private unsafe void FrameworkOnUpdateInternal() { - if ((_clientState.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty]) + if ((_objectTable.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty]) { return; } @@ -639,37 +843,43 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber _performanceCollector.LogPerformance(this, $"FrameworkOnUpdateInternal+{(isNormalFrameworkUpdate ? "Regular" : "Delayed")}", () => { IsAnythingDrawing = false; - _performanceCollector.LogPerformance(this, $"ObjTableToCharas", + _performanceCollector.LogPerformance(this, $"TrackedActorsToState", () => { - _notUpdatedCharas.AddRange(_playerCharas.Keys); + _actorObjectService.RefreshTrackedActors(); - for (int i = 0; i < 200; i += 2) + var playerDescriptors = _actorObjectService.PlayerCharacterDescriptors; + for (var i = 0; i < playerDescriptors.Count; i++) { - var chara = _objectTable[i]; - if (chara == null || chara.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) + var actor = playerDescriptors[i]; + + var playerAddress = actor.Address; + if (playerAddress == nint.Zero) continue; - if (_blockedCharacterHandler.IsCharacterBlocked(chara.Address, out bool firstTime) && firstTime) + if (actor.ObjectIndex >= 200) + continue; + + if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, out bool firstTime) && firstTime) { - _logger.LogTrace("Skipping character {addr}, blocked/muted", chara.Address.ToString("X")); + _logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X")); continue; } - var charaName = ((GameObject*)chara.Address)->NameString; - var hash = GetHashedCIDFromPlayerPointer(chara.Address); if (!IsAnythingDrawing) - CheckCharacterForDrawing(chara.Address, charaName); - _notUpdatedCharas.Remove(hash); - _playerCharas[hash] = (charaName, chara.Address); + { + 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; + } } - - foreach (var notUpdatedChara in _notUpdatedCharas) - { - _playerCharas.Remove(notUpdatedChara); - } - - _notUpdatedCharas.Clear(); }); if (!IsAnythingDrawing && !string.IsNullOrEmpty(_lastGlobalBlockPlayer)) @@ -679,76 +889,75 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber _lastGlobalBlockReason = string.Empty; } - 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()); - } + // 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 ((_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))); - } + // Gpose + HandleStateTransition(() => IsInGpose, v => IsInGpose = v, shouldBeInGpose, "Gpose", + onEnter: () => + { + Mediator.Publish(new GposeStartMessage()); + }, + onExit: () => + { + Mediator.Publish(new GposeEndMessage()); + }); - 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))); - } + // 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 (IsInCutscene) { @@ -782,10 +991,22 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas))); } - var localPlayer = _clientState.LocalPlayer; + var localPlayer = _objectTable.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) @@ -801,6 +1022,7 @@ 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()); } @@ -808,6 +1030,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber { _logger.LogDebug("Logged out"); IsLoggedIn = false; + _lastWorldId = 0; Mediator.Publish(new DalamudLogoutMessage()); } @@ -825,4 +1048,31 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber _delayedFrameworkUpdateCheck = DateTime.UtcNow; }); } -} \ No newline at end of file + + /// + /// Handler for the transition of different states of game + /// + /// Get state of condition + /// Set state of condition + /// Correction of the state of the condition + /// Condition name + /// Function for on entering the state + /// Function for on leaving the state + private void HandleStateTransition(Func getState, Action 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(); + } + } +} diff --git a/LightlessSync/Services/Events/Event.cs b/LightlessSync/Services/Events/Event.cs index ca540b9..9725d49 100644 --- a/LightlessSync/Services/Events/Event.cs +++ b/LightlessSync/Services/Events/Event.cs @@ -6,6 +6,8 @@ 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; } @@ -14,7 +16,9 @@ public record Event public Event(string? Character, UserData UserData, string EventSource, EventSeverity EventSeverity, string Message) { EventTime = DateTime.Now; - this.UID = UserData.AliasOrUID; + this.UserId = UserData.UID; + this.AliasOrUid = UserData.AliasOrUID; + this.UID = UserData.UID; this.Character = Character ?? string.Empty; this.EventSource = EventSource; this.EventSeverity = EventSeverity; @@ -37,7 +41,7 @@ public record Event else { if (string.IsNullOrEmpty(Character)) - return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{UID}> {Message}"; + return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{AliasOrUid}> {Message}"; else return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{UID}\\{Character}> {Message}"; } diff --git a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs new file mode 100644 index 0000000..d78563c --- /dev/null +++ b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs @@ -0,0 +1,692 @@ +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 _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 _activeBroadcastingCids = []; + + public LightFinderPlateHandler( + ILogger 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(this, OnTick); + } + + internal void Uninit() + { + DisableNameplate(); + if (_drawSubscribed) + { + _uiBuilder.Draw -= OnUiBuilderDraw; + _drawSubscribed = false; + } + ClearLabelBuffer(); + _mediator.Unsubscribe(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 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(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 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 cids) + { + var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); + if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet)) + return; + + _activeBroadcastingCids = newSet; + if (_logger.IsEnabled(LogLevel.Trace)) + _logger.LogTrace("Active broadcast IDs: {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; + } + +} \ No newline at end of file diff --git a/LightlessSync/Services/BroadcastScanningService.cs b/LightlessSync/Services/LightFinder/LightFinderScannerService.cs similarity index 72% rename from LightlessSync/Services/BroadcastScanningService.cs rename to LightlessSync/Services/LightFinder/LightFinderScannerService.cs index 95abdae..52ff1dc 100644 --- a/LightlessSync/Services/BroadcastScanningService.cs +++ b/LightlessSync/Services/LightFinder/LightFinderScannerService.cs @@ -1,67 +1,62 @@ -using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; using LightlessSync.API.Dto.User; -using LightlessSync.LightlessConfiguration; +using LightlessSync.Services.ActorTracking; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; -namespace LightlessSync.Services; +namespace LightlessSync.Services.LightFinder; -public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDisposable +public class LightFinderScannerService : DisposableMediatorSubscriberBase { - private readonly ILogger _logger; - private readonly IObjectTable _objectTable; + private readonly ILogger _logger; + private readonly ActorObjectService _actorTracker; private readonly IFramework _framework; - private readonly BroadcastService _broadcastService; - private readonly NameplateHandler _nameplateHandler; + private readonly LightFinderService _broadcastService; + private readonly LightFinderPlateHandler _lightFinderPlateHandler; - private readonly ConcurrentDictionary _broadcastCache = new(); + private readonly ConcurrentDictionary _broadcastCache = new(StringComparer.Ordinal); private readonly Queue _lookupQueue = new(); - private readonly HashSet _lookupQueuedCids = new(); - private readonly HashSet _syncshellCids = new(); + private readonly HashSet _lookupQueuedCids = []; + private readonly HashSet _syncshellCids = []; - 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 Task? _cleanupTask; + private readonly Task? _cleanupTask; private readonly int _checkEveryFrames = 20; private int _frameCounter = 0; - private int _lookupsThisFrame = 0; - private const int MaxLookupsPerFrame = 30; - private const int MaxQueueSize = 100; + private const int _maxLookupsPerFrame = 30; + private const int _maxQueueSize = 100; private volatile bool _batchRunning = false; public IReadOnlyDictionary BroadcastCache => _broadcastCache; public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID); - public BroadcastScannerService(ILogger logger, - IClientState clientState, - IObjectTable objectTable, + public LightFinderScannerService(ILogger logger, IFramework framework, - BroadcastService broadcastService, + LightFinderService broadcastService, LightlessMediator mediator, - NameplateHandler nameplateHandler, - DalamudUtilService dalamudUtil, - LightlessConfigService configService) : base(logger, mediator) + LightFinderPlateHandler lightFinderPlateHandler, + ActorObjectService actorTracker) : base(logger, mediator) { _logger = logger; - _objectTable = objectTable; + _actorTracker = actorTracker; _broadcastService = broadcastService; - _nameplateHandler = nameplateHandler; + _lightFinderPlateHandler = lightFinderPlateHandler; _logger = logger; _framework = framework; _framework.Update += OnFrameworkUpdate; Mediator.Subscribe(this, OnBroadcastStatusChanged); - _cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop); + _cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop, _cleanupCts.Token); - _nameplateHandler.Init(); + _actorTracker = actorTracker; } private void OnFrameworkUpdate(IFramework framework) => Update(); @@ -69,34 +64,34 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos public void Update() { _frameCounter++; - _lookupsThisFrame = 0; + var lookupsThisFrame = 0; if (!_broadcastService.IsBroadcasting) return; var now = DateTime.UtcNow; - foreach (var obj in _objectTable) + foreach (var address in _actorTracker.PlayerAddresses) { - if (obj is not IPlayerCharacter player || player.Address == IntPtr.Zero) + if (address == nint.Zero) continue; - var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(player.Address); + var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(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(); - 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) @@ -118,8 +113,8 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos 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; @@ -133,7 +128,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos .Select(e => e.Key) .ToList(); - _nameplateHandler.UpdateBroadcastingCids(activeCids); + _lightFinderPlateHandler.UpdateBroadcastingCids(activeCids); UpdateSyncshellBroadcasts(); } @@ -146,7 +141,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos _lookupQueuedCids.Clear(); _syncshellCids.Clear(); - _nameplateHandler.UpdateBroadcastingCids(Enumerable.Empty()); + _lightFinderPlateHandler.UpdateBroadcastingCids([]); } } @@ -156,7 +151,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos var newSet = _broadcastCache .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID)) .Select(e => e.Key) - .ToHashSet(); + .ToHashSet(StringComparer.Ordinal); if (!_syncshellCids.SetEquals(newSet)) { @@ -172,7 +167,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos { 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 { @@ -180,8 +175,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos IsBroadcasting = true, TTL = e.Value.ExpiryTime - now, GID = e.Value.GID - }) - .ToList(); + })]; } private async Task ExpiredBroadcastCleanupLoop() @@ -192,7 +186,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos { while (!token.IsCancellationRequested) { - await Task.Delay(TimeSpan.FromSeconds(10), token); + await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false); var now = DateTime.UtcNow; foreach (var (cid, entry) in _broadcastCache.ToArray()) @@ -202,7 +196,10 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos } } } - catch (OperationCanceledException) { } + catch (OperationCanceledException) + { + // No action needed when cancelled + } catch (Exception ex) { _logger.LogError(ex, "Broadcast cleanup loop crashed"); @@ -235,8 +232,15 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos { base.Dispose(disposing); _framework.Update -= OnFrameworkUpdate; + if (_cleanupTask != null) + { + _cleanupTask?.Wait(100, _cleanupCts.Token); + } + _cleanupCts.Cancel(); + _cleanupCts.Dispose(); + _cleanupTask?.Wait(100); - _nameplateHandler.Uninit(); + _cleanupCts.Dispose(); } } diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/LightFinder/LightFinderService.cs similarity index 95% rename from LightlessSync/Services/BroadcastService.cs rename to LightlessSync/Services/LightFinder/LightFinderService.cs index cca9af6..82a51c7 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/LightFinder/LightFinderService.cs @@ -1,22 +1,21 @@ using Dalamud.Interface; -using LightlessSync.LightlessConfiguration.Models; -using LightlessSync.UI; -using LightlessSync.UI.Models; 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; -using System.Threading; -namespace LightlessSync.Services; -public class BroadcastService : IHostedService, IMediatorSubscriber +namespace LightlessSync.Services.LightFinder; +public class LightFinderService : IHostedService, IMediatorSubscriber { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ApiController _apiController; private readonly LightlessMediator _mediator; private readonly LightlessConfigService _config; @@ -45,7 +44,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber } } - public BroadcastService(ILogger logger, LightlessMediator mediator, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController) + public LightFinderService(ILogger logger, LightlessMediator mediator, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController) { _logger = logger; _mediator = mediator; @@ -58,7 +57,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber { if (!_apiController.IsConnected) { - _logger.LogDebug(context + " skipped, not connected"); + _logger.LogDebug("{context} skipped, not connected", context); return; } await action().ConfigureAwait(false); @@ -282,7 +281,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber if (!msg.Enabled) { ApplyBroadcastDisabled(forcePublish: true); - Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}"))); + Mediator.Publish(new EventMessage(new Events.Event(nameof(LightFinderService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}"))); return; } @@ -295,7 +294,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber if (TryApplyBroadcastEnabled(ttl, "client request")) { _logger.LogDebug("Fetched TTL from server: {TTL}", ttl); - Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}"))); + Mediator.Publish(new EventMessage(new Events.Event(nameof(LightFinderService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}"))); } else { @@ -372,7 +371,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber public async Task> AreUsersBroadcastingAsync(List hashedCids) { - Dictionary result = new(); + Dictionary result = new(StringComparer.Ordinal); await RequireConnectionAsync(nameof(AreUsersBroadcastingAsync), async () => { @@ -397,8 +396,6 @@ public class BroadcastService : IHostedService, IMediatorSubscriber return result; } - - public async void ToggleBroadcast() { diff --git a/LightlessSync/Services/LightlessGroupProfileData.cs b/LightlessSync/Services/LightlessGroupProfileData.cs deleted file mode 100644 index 1b27b40..0000000 --- a/LightlessSync/Services/LightlessGroupProfileData.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace LightlessSync.Services; - -public record LightlessGroupProfileData(string Base64ProfilePicture, string Description, int[] Tags, bool IsNsfw, bool IsDisabled) -{ - public Lazy ImageData { get; } = new Lazy(Convert.FromBase64String(Base64ProfilePicture)); -} diff --git a/LightlessSync/Services/LightlessProfileManager.cs b/LightlessSync/Services/LightlessProfileManager.cs deleted file mode 100644 index 00b610b..0000000 --- a/LightlessSync/Services/LightlessProfileManager.cs +++ /dev/null @@ -1,191 +0,0 @@ -using LightlessSync.API.Data; -using LightlessSync.API.Data.Comparer; -using LightlessSync.LightlessConfiguration; -using LightlessSync.Services.Mediator; -using LightlessSync.WebAPI; -using Microsoft.Extensions.Logging; -using Serilog.Core; -using System.Collections.Concurrent; - -namespace LightlessSync.Services; -public class LightlessProfileManager : MediatorSubscriberBase -{ - public LightlessGroupProfileData LoadingProfileGroupData => _loadingProfileGroupData; - //Const strings for default values meant in the profile screen. - private const string _lightlessLogo = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDIgNzkuZGJhM2RhMywgMjAyMy8xMi8xMy0wNTowNjo0OSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI2LjggKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyNS0wOC0zMFQwMDo1MTozNCswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNS0wOC0zMFQwMjoxMjo0MiswMjowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjUtMDgtMzBUMDI6MTI6NDIrMDI6MDAiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Y2JjY2YwZDctNTU4ZS1kYzQ1LTk0MmQtZTg5YWE5ZWRhYjUzIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6Nzk1NDcwMjYtYjM2OC1jMjRhLTliYzEtYmQzMjE4OTJlN2FiIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MGRiOTU0NTktM2E2OS0xNDQ2LWFmYmMtM2M4ODhjZDUxYmU0IiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MGRiOTU0NTktM2E2OS0xNDQ2LWFmYmMtM2M4ODhjZDUxYmU0IiBzdEV2dDp3aGVuPSIyMDI1LTA4LTMwVDAwOjUxOjM0KzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjYuOCAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmE5OWYwOGE5LTgxNDItY2E0Mi04MmY1LTM2MjZmOTgzNDZhYiIgc3RFdnQ6d2hlbj0iMjAyNS0wOC0zMFQwMDo1MTozNCswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI2LjggKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpjYmNjZjBkNy01NThlLWRjNDUtOTQyZC1lODlhYTllZGFiNTMiIHN0RXZ0OndoZW49IjIwMjUtMDgtMzBUMDI6MTI6NDIrMDI6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS43IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5CIHRlAAEVd0lEQVR42uz9d7xtWVXmD3/HnHOttcPJ5+ZYOVAUFDlnEBQwKybs1jaDoj9b36bbbIvaKooSDC0iQWkkqIAiSJCcQ1E536p7q26+J+6w1ppzvH/MuffJt24sSas++3PqnrPTWmuOMcd4xjOeIT/++F+5F5ERQQEBWPq/+E8k/VxxpF/KOs9f/oKl/xUQZelV6Vcs/zvLf4Gk1wx+KbL0WRr/Gv8usvxliMjK90mvG/5txXcb/Dt9t1WvHbxYzPAboUtff+U5rnfeg9en77niiq26viLL78Lq91n+C1l7T5Z/7cH1WHPt116X1RdWVt1bVnxlWVohw+u0+rNW39fB++mKa7/8u8l63ym9yeB6i669XisX6apfKTC8Z7rmS4lstA7Teco6y33ddb3ye8iy8117H1Ze3/XvxfJrMnifZee/zvVZeQkGNrNsrS//fjKwcEBkwQHbl8xj6YXCst/qshux6myWG78o6HJjXHGTFUVQdNXFW8+7LL8ra2+wCpi1H7LMSFj1HYXV112HzxksjnVW0OBXZtlzRZZ8kmyw4tcxTkSS01KWX9zVa0HY+H0GDmJwg1npTtPXWOd6JeNTXfKnsvr6r7rcrPSNqxzjMmMcXidZe87EhTs04BXfaz3DHS6idYyGlX/Ttbdu7fKRddfI8g1EZO1rdJ1lqCe7R7rKGNPvZM0Ouf6bDI09Gefa85HkzFYbv67jhHWVUx0Yqa73oaMGYX6lFa8wi+gMhLUnOHRISy/QVYtxufGz7P02PJZ9wHpPU1l2Y3W9CyJr/hbXja65acMbvcyQdVXYI4Oz0xVLdp3FqSuuq6zjvJaMf9mKX/V5uirqksHqNrLC2GX5OUva6UTWGNUaY5VlBrNq11r5ddder/Ucw0q711VOQ1YuXFll/LLKGAa3bZWxDB21rFrYsuQU1jPYNTv08rWDLLsGa7+XrHn+2vfT5Q7YrPyMwY6pq99fWD8SSechrHZAuu7esuSLl61R2ThqXHHfV57wvJNlnmdFuL1OyB/37zVmufaT14R3MtxEVjrO9Z2BrNhdlp/N0iJQXW9N6nqx2UrvKcu9//rnKuv94mQhI2s9n6bvuXIDkDXXZXmotjwVYN3Fk6KfdTdc3TgSWRXS6/LvsDzUW9q2Nwznh5H16q1tzTVZfy3JSULx5fd7vV1w7UKXteknqwKIVWnNcA0Ngoh0PdesRV0/CpVVS0JXfIdlIbusXOeyylbWGOMyByor7G11lLc2hVweash6uZjousGqAm5ZdrzBgl61qNeLD0+SCw6iDzHrOZT1dpSlLH95tCeyLJ/TlTnS6h1tw5B2Vei5MhVYuzjZKFhR4q6sK6MhXeboZJ2dZU3QvixkGxo26yxEWZXzsX7+udY4Vu0gyy6mkdULfflzZX38Z2AsGznBkyxQWe/Z66RhsiaO15Xh9PIrqKs3lbW754a59oqvKuukAqzZEHXFizbO9nS99GY13rEsfdH1HOM6+MjyzVCW3Yel91q9/nV16LUmXXQrgI0lD6rAIqyz4a9axrqux151I80G4MuqnIwNsAFJJ6MiK4CHlang2uhjvRx6w1UoG4Yha2CBoddf8/66gTGe7ONk5SVadwdfP8qQ5eHvqt1AV2M5g8sm6wOI6y0YYb3zGeA5utYpLTeOFfa/OhFeD3hjLXC5xjBOBpishg5k1c6/BqlemdKuxg7WvWcrP3g1JjbcuXWl29BVjnO1lay4bCsQ6+VOTdcLupYZyNoIUJcHf0J7HSQAt0EYu6hwmaALa0KmjTZGPcmOeQrPP4lprgt2rRv7rX6tsnHIztqcc91T0A3c/KrnqNz/09amTcIZftxpXer13mzle5zuO66Byk7rXp/sTp/am60Nds/ZVzkXbyBrLfCkL9czvI96ipdPGAFuEWRkeQVHBg5gEAovAX8EhfvO+Mt94/jG8Y3jK+cQ5oGwXh5gWJUHD4IpQUfP0ld+4/jG8Y3jP+sYbPQGBEaXY6GyDG9zK4BfkY3qhd84vnF84/hq2vSNIFbWRPArrFsFNyhbroGu7yft+MbxwB/B+2U5OIhRVJW69kOuRbx9Zui7zSD2SyijWQf0HeR9q1mZK8qwJi4UazKMs6gGVoOAxrpv3KSvgJ2fDYxfVv1KDLhYUtM1O7+cDwDlG8f9GLeiEvFiX1XLAGtFxFK0WkOUODrjgDGWqa2bMcYsI0fIWqctG9/XFWXcjRaAgDGOfmeebm8Oa7OhAzAiqCpV1UMJw0qJNRZjbNpg4jnINyLK82f7qhhrMFY2pHbIgKWYfuFWsEOWJQq68ad8wwucUUwmqPcEDYgxaF3jg0+e2NBIxh1UscYwtn03xhhU4y5vjGFsanMyKB3eCxEhb7ZWlo9WVsNXVETOOIJTEDHU433qqo8Ys8w3CEEDiwvHQT2IwVhHvzdHrzuPtQ4xgq9LqrqM39UI1rglfofYb6yRszH+AMYKxsn61YG1zKjkAFbVcxMtZWMb/4bxn+KOXg8NL9Q1IXiyRpO80aSuSkanNzMyPkldlYi1jE8n4w4BEaFotpfIPOl96rpa1wFX/d4DFl+KGFxWrIkMHTA5vWup9UaEut6Mr/uIGIyx9Hrz9HvzWJuhBLqd48mZCVXdRVSHjmHAcf5GxHAqxq8YK7jMoHqySE+Wc6sQYRkIyGpig95PovENbzC8qN6j6kHA9ysQIRsdSXm6YWzLNghKa3SMkfEJfNknLxrkRQMNAQV8VSXiZzT6st/9SjzZFJH4dVdECH6ts3DFMDxttSYZGdk0/He7vSm9Z6DbPRGdgSjd7onoOENFXVdDB7KEbXxj3a3Y+Z3gMrcuUX+jzXvwv245I+p0qRX69WrwIUBdQ1Wi3kOrjWk2ERFae7fhspyRzVtxIhhVGs0mVixa9dHKkzeaaAj0Fhe+hqssa52F9wHvq+G/jcmGkef4+M5ht2irtQlBKMsFynIREaHXmwWJTsH7EhGHMYMIQb5OjV8xzuByGx3uSUG6QWy/8jlu3VxBv7G7Lzd48TVCgLLG9Lr4kRHC+DiyfQfWe7KpKYrJaWxdUbRGyAhIr48NilGl6nWpFYzGm6RB19JUvy5BKz/sIg3BDzcUYzLQQNGYoNmcJGigUUyiotR1l6ru4n0f73uoBlTrBDCar5t1OzT+pgXVmBmeAttyeU+FQCwDrgjbUmPK1/th6xpblthel3JsDF/k1Nt24CenMGPjmK1bsXmBM4Lr95B+iclzfGcR0YDV2IaMKub8f91xYCuwE9gMTAHb0s8WMAaMpv/P02PwtWqgBHrE/o85YCH9/yHgMDBDZIYeSP8uz79jiNFD0JgiGWMJBPJ8lDwfw4eK4Gtq36OuO9R1F1BU6wQqfu06Aw2KzQwud/E6BQG5/4h8Td/Ycgxg0GO7osPo62UnEsH5GhMCWb+P9TWLo2OcuOgiepOT6OQkftNmcuew7REanUWKfh8WF1KYG9CgqTZ+Xo9dwJXAFcDF6bEX2JOcwPk+quQA7gbuBO4AbgVuAm5JjuI8OYWAEtKOH1K7tcFlbTLXovY9gga874J6fOinkuQa9Zav/p0/M2QNd4o7/9q8f3njnVtu7cP+av36MHrrPVlVM9JZYGF0lF5RcGjHTo7s3cPi1m348Qlsq8XY4gLtsgQfcMePDXf283xsBR4JPAJ4MPBQ4EIg+0+8bFmKMnYCj1v1t3ngRuALwHXAp4Avp8jifNzB5AziTxGDEYPIGBpqRBr4UKKUMX0jLJUc9atzf1MfjT9vZiu6MU8LsZMlibRYBVgFAiyJEZzu7fjKhwYHRp9XFe3OAp1mi2PTm7j+qqu4Z+9e5jZtxhQ5tNpMd+Zp9Utaxzo41agqNHC358f4twBPTob1BOAaoPgqWp+jwKPTY3AcBj4OfBL4CPA5oH9+IoTYorwEPAoiGaqWEGqUCkJARLEmMSKFJU7FV8POn1vypos2dtrBpoBZqt6JLAMBxbCqz1jP0oy/8jiDtq5pdzvULuPopk1c++Cr2LfnAg7s2ElndISiqpjo92l6z8jxYzhRJCQNgvO3SB4FfBPwVOCJQONrLNDaAnx7egAcBN4PfBh4T0olzpfJQNr5o4OwhCCoekr1WBfIrMFag7WCD/oV6wzUK7YwFC0XW/KDrhXYOaUEYK3SqVtPbEK/FgCAVK4rFhZwwVOOjrLvgov4zKMeyYGt25gbHcHVNa1uj4mZGSyKUzBD4ZHzdg2eCDw/Pa7k6+vYBvxgetTJGbwbeCdw13k2oyhkohAU6srT69dYKzgrFLkjy5YM7CvFBoJXbG4oWhmDkslZ8SBWvdTJOgTwc1Od+s+LAqSsyBcWkCznyGWXcWT3Lg7s3Mk9mzYzn+UU3UUmZyJe9QDhHQ8HvjvthOfb6Htppz0EzAJH08+uqs4552Yzl3dVQymYwLIuEBHJq7osqroaFZEJoAmMECsLE8Sqws4U7p/tzXXAs9Pjj5MzeAfw9pQ6nNe9wSB4gaqs6XpPx0LmLEWR0ShynHOEEAgh/KcZv3rF5painUg+Qc/upFerN6kuVwRaV+r1q2i391BW2H4Pv2maE1dewYmdO7nnisuZzQvqToei18N0OtTmgTg/3QR8D/ADIE8cahyemzLLAnAbEXm/0zp7uyD7Be4JGg6LMceKPPNGHEZMkgU3NIsWM7PHOT5zlCzL1oS8VV0xNb6ZLdM7qOsaRKgqjzGGqu4jKP2qNw5sEZFdwde7+lX/AtALQS4GLgW2n0mGllKhbwL+KDmCfwD+6bwuG8AYwWHwoWZhsc9CJ2CM0Gw0GRsdoSgKfPCRsfkABgXBK64wFCPJRAPLtfDPbvMfYHxmGQgopyKG+RVo+BoCOj+PzTJkYoz+hQ9h4UFXcnzLFhbqmmxhgVFdYNEYSmRDJeJzaPhPReSFqvq9GnfPs42HThDR9C8Ya24wYq5FuNll9oSzjspXdOd7hNQj4KylN9/l1kNHKEOP2pc4sVR1j9L3OXTkPm6841pGxyZZ7B6NW7Ez+Nqz2Fvg0r0PYu+OS+j2F9BQMzbaoqpKRtubAMsFO6+czbN8tq6rW63L2Da1k0bRwoeaxc5cUdbVhQJX98ruVaBXgzyCWKo81aOF6iBNuAl4Y3rsO79LSXDO4bWirj3HTxxnbm6G0dFRRkdHaRQFxjiqqjzv6YEOjH80B5RB5/W5cXnLV6MgP/2035iTGNIt10ucT6He/DnDY1Zro69S1F1P0nrDSTfJ8MNiBxHI9+zGPOTB9LdsZrHdpqw83apkwRg6xtAVQ0eEBYSexNBv+NYatx+L0lAoUJqqNAkUQWmo0giBhip5emQhDH86VTINuKAvtKo/YUN4oiMyAI0qNj0kgBAwOgBjN2y4OozyUbHyKWPMJ0G/YHM3n9uChbn52EEY4OjBE/TqDg3T5uB9R5nrH6Xne4xlOxlrN7jvxCHum7mHTm+WVtZkduEQt+6/nq3T21ATcEXB7Py9gJJllqosMSajVy1w9Oh9iA1oHbjggknm5me5+rKn0y09vX6f4zP3UdU9Lr3osVy6+yp63RnqoDz4ssewZ8teyqrP9s17ybMG8wvHXL8uLxXkCf2qdw3ok0GugrX8qKVmRkUJKeVVgJqgbwyEvwQ+oYkcFPsoFDQkwlCqAgSPaiAMiEQh/r9qDOmDepRACPG5g9/Fn3UK/T11XVFWJSJKs9Fg0/RmxscmEGMoyz6BgFl2GiKsM6WHNaKgK2RJV6toe8gKS2MsG+IR5+AYBQ6AjA4WfcIR5tenAp+PWOsc7vjVwiJGoL1nF80rLkcv3EvPWej2MfPzsbX0AQnzaQr8KMjPgl4OoGII6jGr1fRTt9UGu8dngX83Vt4vznw0K1zPdzy9Xh8jhqN3n+DE3BFkoWD//N3kFBRhlPsW7mbnyEWY3KFBKMuaTtWZbjRkS7Od7xypm9N5oRNo2La5vXWqOd5qOtds1b43Yp1rTE1szkWMFaPG2kbwdc8HrcsrL8s74DvB9ztbp0c7dX/xWKMxfmi+2z1x98F9R2fnj9+r1IeOzx2eeeu/f4L5IwehkXPtndcye+w2Jqf28PhrnsP84lGuuuRx9d4tu2/sV+WNOzZfgIiw0Jm9uF/2nl378oki5pkJYxga/5AfvHT5HPBf0+PdwCuA953vm2uMIcsyvK+YW5hjdm6GkZERNm/ayvTkJhCJjiB1cJ512F8rWeOcG//ynXcVBV2WgYCrNOXPb3nmNAPiZPjl/CIiML5rJ+OXX0q2dzd96+guLsbQ3jxghj8B+tMKPxOEXUEhyPLBMNHQB4Uos1xFVyAolVHei5H3Gifvsbm9xWLpd0u6B7sszC9QLXiOHT2Bywx51eL6Q1/mIbseh3hLt1xsjkxMXlJodsUxPXBp3asu6oWFi7yEnTPcufPgwV4bEYwBFU/ta0bbY2yfmKDTrwm+xBqDmAwjQiCQFSOEuo9qTdFoIoB1jsI5Wo2C+cVZ1HW4rL2dB13xDMqqMzc7e/ieifE9dx/ffNed1jVuD1rfutjv3Gg7s/ve8r7XVPPHD3Ppg29iLOuSuQku3HYRYyOTXHXRo2/fMr3j1dtH97x6vjPT7PQWnl7V5bMUfZ7AxUsd0Eu6B2Gp8P3c+ND3A38K/PP5zzSFPMtR9czMnuDo8SOMj02yfdsONk9tRQR6/V4iJJ3Z+gs+Gn9zPE9RzLnejNfRURaQn3n6b8wBo6tkAOdBzl0KcLK04CQpgBEhhEDZ6SIiTO7czvRlF9PevYvKWDq9LqUKpTP0raUvhq4R+mLpGDkfKcBoHsLPZuhLnOoWFxSnitOYCrigWA3YAI6wXhrw70J4uzXmn4umPaB9T3++pHesy2K3w/yRDqYHZb8ma7ZoNkeY6R7OjQmXX3foU4/OXfPqfGTioQu9masC9Waxll41TwiKETscLRU0YE221LlohKru42yOMYKxjrquo1CHRNMSE2vkMSw1NIqCVnuSbm8B5xyqln45D1px4aWPQ33N3XdfTx0CxhjKfp9ubxYRUUX3d3rHr+2XvevUNL/QW7ztU9aO3nXXPZ+jUTQZH9vKgy56BDumL2Tnlou4fM9DmBjbTFmXZnFx9sll3ftW4FsVvXig3a6qBHQYIQxZgPBRNPyfoOGd5zoFCMHjg49EokRBDt5ThopOZ5FAYHpimp3b97B5aivGCL2ySwghSa+dWgrgayVrWloTRfr+eq4bxUZRDoikVH8YBei8/MwzkgNg+aQcObcYwHpe9WQOQGKPeLXYAREmd+1g26UXM75zO94YFruxu65yllIspZXz7QBsQ/XnM9VfzFS3Z8rQ6LMQhgaeqWKDYgnYEHAKVvVGq+HNzsj/s87cnGeW6liHhYPzlMf7dI4tQgVzfoGJkc20WyNurjzx0FuOfOkpRxfufvRM/9gTxdqd7ZEpghjazUly16Dbm8UahxWblIUUHwKgWOsIwWNtjnU5/f48iuCcQVXwIaTBRh4xeTT+NGHJGsEYg8uaWFdQVT3AU/syNaHkGIkOxYfYidfr9cjzVurfr8EYGs1RRDL6/S79zmGvojctdo5+rK47n53vHP1At794++zcYabHtrFt8yVcefEjuHz3VVy080GMj2yiqntmoTP37Kruf6+qfg/QDinfV015/2AnUSVoeK+iL0P1P8J5dADe19TpNXWo6XQXEBE2TW1h9469bJ7egojQ73cJQeO1PYkDCF5xDUtzIukmhPMCMI6KcIBlWF+KBgYOQEZFVqB1590BrAX7BDFpx1/sgBGmdu5g62UXMbFzByJCudihUqW2hloMlTXn3wEE/cFC9VcL9PJMo1FnS8BfigBiFJAFxShYDbgQ3urgb13DvqswwEKf+miX2YPHKQ93qGcCzdYIo+1xvAtb9y/e8ay7jtz0lNuPf/FZM92je9Uo7eYUWyYuZNP4LoqsQVAoqx4+1DhjqYOnDmXUGiAuVhFDljWpqt5wxuDSyDKNz6trxDiMcfhQRoN3OV5DFP60JhqCBhqNNnVdE0IVDShOIkW1So7EUNcVXgPGmmj03UUQj3UNxESwznuPMQ5FqMr5UFWdL1X1wvs7i4fft9Drfqjv58qRosWenQ/iwm1XcNGOK7l419WMtibo9Rc3d8uF763q6r8I+qjhTp/qYxodQIosw1tCCL+uGm46nw7Ae09IzUkhBHr9GKVuntrCrp172Tq1BcTQK7t4Ddhlw19lWc6fNx3NyQLV82b8Sw5gGAEMk4F5eVFyACvnuD1wDiCKmAohKGWnAwJTu3aw9dKLmdy1IwIti50h0FKLUBs5rw6gpUpD9Ym56u8UGp6cqVKkR6ZLRp8t2/UzDdgQjjr0ddaav8xb9lbX98idM9SH5lk8OktY9HQ6HaYnt9JojOw43Dvw/NuOffmbv7D/Q88+NHtnQ0zG9MQO9my+ih2TF2Ntg161gCtyqtrjfYVzLpb2jKWua8Q6MlfQ6Z4gy5tUVYn3VTK8pCeY5dhUy8+ygrwo6Pc6VCGQZwV1GR2utRbvAwhkeQNBKasuIhk2c6gG6rrC2Iy66uOTCOVgxxMMiOLrPsGXGJsRUIxtUpUL2CxHyFDfQ8QhYqPoR92/N4TFd3a6h9+p1v/b8eP766Zts3lqBw970FN52KVPZGpiB5lxLHROfFPpy58KGr4jWlHAD3ClkKICDT5o+C1V/1vn2wEMVJAkpavLHcGenXvZtGlrXMP9Lj6EKNdNDPvzhqM9VZx7wG+DKsAgBdClsv+8vOgZvznHitwgOgB9gBxADINq6rJiYsdWtl12yQrD1xBW5EPn3wGErS3Vl+Xwo1kq+xUrfoZo8IALgRzFBr3NhfCqLDd/4wo7mx9ZxNw9QziyiLlrgcoHGpNjmCIfPdy97zvumLn+O2489MlvufHgp3PfDYyOTvCQC57Cjs2XU2QjVHWs69daIgh5a4JiZBoNNd35IwRfRmWc4LFZg8w1yPIG/d4Cvf4ckdBcoSFKkhnrErMNyqpPs9lCNVCWXcRlZLbAhzqW1SSQuRwf+hgMAY+HKPQpjqosqX0PJCAmxxUjlL05QqgRyfC+j3U5xhh8OU+tGnUOjaAqiLEInrqq8KFOJV4L4vC+xDi9p+qf+KeF+fveMb948AP9eoGJkW1cuOsanvqI72DH9B7arTEWe3PXdLsLP6XoCxVtobrUEBR8KgmG64KGl2oI7zrfDmA5YDh0BAibp7eya9cetiZH0Ot38aUnbzvak82YvoTzLrS7VAaUFen3MgewcvDgvDxQEYAqVa/HRY9+BNuvvGyYAmxUWjlfDsChFMp/zQm/n8OWRtrlC9WooKFKoYEigX0x/9cbc8LLbW5e6/IsNI8tUNx0hOzWGcLxDq49wsTEZo7Ux55028y1P3jT8c9+9w0HPz194thRsHDBtit52KXPZvf2q5mfO8xC5zi9uoMzBdY5Bl3H7amdXLTnUgie2+65le78UUR9NM7gyfIWWVbQ6ZwYEkdclhG0BjEEXxMginFqnbQHbay1i9BsjlNWHYKvhzMC1PfJGqNY6+j05lMCGwgaqwN11SUoiM0ikU97iS1oMXkTxeDLDkJNq9GkrCs6/R5WHMH3EGMIaqIeYtkFCRSNUcp+TZBAqPt43/tyvzz6d53FQ28uy7m7RrZdzkSWc+X2B/PIhzyHLRPbme/O7Oj05n9avf60ik4PMILBzp8cwVuDhl9W9XeebwewniMAYcumrezZtZfpiU3glGLcxc0vnHO4fwMHoAdEZHRVcW1eXvTM31xRBUjI5AOWAvTmF5ncvZ0HP+tplJ0udVWdtJRynhzAwy36+zn6zExj03tDlRwlD+nnymjgThf0901u/iJvWkaOdhi5/hDZTYfJZz1j41vRVta6a/G2H7ju2Kd+9Pojn37cHUevgy5QwIN3PZpLtj2K8fY2XGOU1mibgwdvpt/rk+cF1hUJ2PIYW5C1x9i5bS8SlHsP76fTOQ4akh5phbF5mhHgKKsuqgFrDD7p8TlrCRqoUvhujKAE8qJJVXoq30GMw7mc4GvGp3dijDBzbD9el8bChrqPGnB5i+mtlzJ74iBzJ+5GbIb6CpfliGRUdT/l5LFPXwTKskf81IgbYGx0AL6MqUUVy2hZ1iAQ8GqjNFhQqnLOh3rmLYy0Xjt39IZ/nz+2n+07H86jrnwaD73s8Wya2EanO7epVy7+bFX1XwIyHvGKVCmIjmBe1f9i0PBXUX7s/DqA9RyBrwMXXXgRj3niYyirPr4OD5QqXIwAZDkISCICrTs9+YFp5FGNbY3bL7sUDeF+jf88Hb8G/KYi1AN+OEolYFWoBayCF6hVT1jh/1DYl4ciK8ePLjL1mf20bjhEY6akNbmNajOXXTv7xR/73L4PvvD6o5/aNrc4AxXYHC7aczVX7Hoim0d30y3nme0cwZZzHD9eYbMYpnsfEKnT7uxivt2b5579t8VN2FfRkDQkvoFgRGMpT8AaS1DFGIdIRggVGENhC1RNMswMEU+v2yXLMnLTTDu/IctyQl0SjMG5jFD2Y9guFlO4aKjeR+NRjxGHsxa1GQl4QNVjk8R50Kj3J8aSAg7EQF2ViQuo+FCTN0bT9lMRKo8REPFUdQeXFdbbrd8vwX3/xPg1H2+7yT+fWzj4hvd8/LV86dYPc9XFT+SqCx92dGps86+PNMb//Ojcwf+foC9ZkRagowp/CXwH8DOc9+7DZWtchFazTV3V3H7n7Vx46QVs37WdhfkHUBR2jZB3zAXkRc+KEcBKvbDzXwZEhN78IlO7t3PF055E2emeUj/2OYwAHirwSpQnSgxiEWJunytkxHC/APKgFKqvyaz8lm3Zg5Mnuuz80iEmr7uP/ERJa3Ir3bx66LXHP/OSTxx8z4/cdPQzaA2ZAR9g8+hOrt7zFHZPPoh+3aFTzuFcllxNlLcyaUhGzIsNRTGCArXv40wc3+AlpkWSyqQhDPTyZFkvQIb3JXUIWOswNoKFqjXGZWk39FjXREONdXnU0ROofYkAtY8imy5vUPt+qqsDxmIEfKgoqxoRHVYQ6lDH54mlLjsoSpYV1FWZqhNJtcdkBFWqugtBUWOoqwqbxVQi+B6+6g8bmGLFIaL5iCXLR/B1nzp0bhB/4hULCwf+dm5xvj/ammbbpj087eHP54oLH8nxuYNXdXvzvxFUvxs0Xp+0yys6o4SfCEH/4XxHAKujgYW5BXbu3clTn/kUFhc7PEAdRhEEhNEVDkeYdyirxAUeGI8UB2DA1ksvGYZJD+Du/wvAy5c7xwBYhIBSp/+vRXAhvK828qvleONTLeCyz93DlR/YR2Pek49vYnZz92EfO/ahX/z4ff/6g7cduxaAsWKUrp/HmYxr9j6VK7Y9FjGG+d6xlJNarMmxNqNfdXFppl5QpchHCaEmcwW1L8mzHF/74cwAk5glscZsknqDYCQCdkgcrOG1D6r4usKIILaI6H5jlLJapPY9rHERE1BPXdVptp9iU0rh6zKW+QjUwUd+Q15QB4sxUXUnhEBV9YfjvwCsK6jrkrr2kTeggbouESNATV1VuCxDxNLvdWk0mkCgKntYlw24PwlYjGiVTQZZ92cScan1IHHtv2i1R/9nnh97Rb939FU33P6p8t6Dt/DER3wrV130yOunx7d9z2Jn9lmd/vxvqobHDXsM0AlV3gL8FfDTgH+gooHWSIsDdx/g3rsPsGPvThbmFh6Ydb8O3V8At4Y6Kw9EAiD0FztM7drB5K7t9Bc7D8hFUNgl8aY/Z0VYtMwJDEhStcohi/5SZ6R4Q2HhkhsPs/cTt7Dj9jl2TF7GkS3dKz58+AMv/fB97/7hO05cB8BUc4ralyz05rlw81VcvfOpbBvfy1z/BN3OAs5m5HlOy1oWuz0QKLJmBEJ9GfVrQtwpe905lIB1OdY4gtb4EAjDUL2JD9WwNOg1RPAQJViDI0/hZ2wHHkC/WdGkaDSZnz8YpxUVbYy1dBZPJDyhoNls0e/PU9UxFDdiaBSRPeiDkllH8CWqirNZkvVWvO/hQw+XtTHGQapEaEoBYlutxzoX59PZDJf5YVIqxuLrGoJiXJ6io4BILEFal8dSGgqhRwiKy0b3unzs5a6x9cWN1rHf687v+6v3fPyvuf7OT3PF7ofx4Isf+b7Rkan3zS/O/Fgd+r8LbIrgqoKGH9eoyvQjIF98IJxAnOEIt9xyGzv37sRa84ApESms6fq1j7n4aS8FilUyAKUgLwcplxju5+6hKKGquOCR19AcG6Pq90/ZAQSRpYcRvBi8Ebwx+MQT8GKoRChFqNKjFHluQP5VhYeG5RrpqxyjILigb6oK9+2Lk81PTM31eMY/3cij/uVGpucsZnrT1Bd6X/jfb7jl5W/4wN1vv+ZE7zCTjQlGizHmeseAwKMvfB6PufjbKVyThf7xRMON1FwjBmezmEOnphcfPJktaLfaOCvUvo47sbFpKKdNcwBN2uEt3ntG2w1azRYL3UWsMRB8IlMpRoQ8byDiAI/BYG1GWS5S+wpnMpxtIMbg/VJJzhjBhzrW6Y0QRLG2ETsZE73Yumio1sVavmoAjeQilzcQqpjzEzEBY/Mh+URcgXEN1FeR3y9Q1yXB17RGRiOrUYltcRpQ0tALk0XnaAQjLs5XDIoRYrXDFJNFY9PzbTb+be1W+0hZzd94422f5ca7PslIc5K9Oy77vLP5mzr9he0KVw/xAdVtiv44cLeqflEHwOGKRxgCoZFhqCs0CE/3yPKM40ePM71pmukt05T9ctmAk/PyKEB+UUSKlUNAKd1q1eTzrQciQH+hw+SuHUzu3E7ZOb+7f7o9L8vQlybtY5QobL860lH0HlVeNDvZfudYr+Y5776JSz97K3s6I7D9Er6w+LkXvf/WV/7Ktcc+uQ2FycYEzmbUvuLYwmE2jWzjCRd/B1tGL2J24RC1VmQ2o3Ct4WLXIPT7FXlWpJ0zpFwXjFhqVZxNt0XMcLKOD4HMZZHej5A5Q+2h6i3ijIUQUghviOrdFhFHo9Wm6hv6vS7tVkFVC4u9Hs46rDNp0KfgXE5VlUlZ12KsQ0IdiTshioU41yBt6WR5A/AEdUvcfGMxNo9UWa2xtgGSpvd4hZT/Bx/nIWJzbKjImjmuaLO4cDRy6F1GqCuyokBNQd07QaWKGAe+Imgd8Zosdc3VHms8dd1FspGHFdmWt4XyxPsmvfm1hYV7Pvnuj72Bm/d9lodf+dR792y7/AdnFo7+XV2Vf6ZwYWw7DlZV/wZ4JMiLH7Ao4OZb2bln57qjvM+XJayo9gvYR1/ytJeKLKnPJk9Uphz5nA+A0JSXXvDIa2iOnt7uf7oRQF9k3CPvQPiR5QNPNNFjw/KdX/VttbPP60yMfrGxsMD3/sPn+ZYP3kezvYV78rln/MP+1/6/t9z5yh8/tLB/ZDwfZTQfw4ihX/eY681x+dZr+KYr/xvNfJTji/dircWIxavHmjjbLgSl0RglL0YIISrtxBsSS35VVeN9jbMZRWOUuo55vBEzHAfeao4PUG1ELFVZxU61BA4iYE0E+6xrML15D2XZpapLIqfKYozBmDy+h5HhrpZnxTDiqH3JyPg2irxNtzuLqmKtDFWCBPC+TgCfYG0WQbOURkgIWAtiXDonC8ZF9D+lI8GXGCOMjU5i84LKW1ChqrppTYSIyqQGH4GlicIm9Vimmn9Qjc5UK0JV4skuNm78xxrFyKSz+tH7Dt9UXn/n55ga38H26T23IvI3ZdXfIxoeskQtDo9G9VmKvlNVO+crAlgRBWyeZnrzNGWvRM7fzlsg8osghSzX1hAp7WMuiSnAsEEohgclwsvTz3MWiYhAf7HD5M4d7Lr6KqrO6Q/APCUHYAx9Yx4ZRN4nwqMHLm8wEEFXjkjumhB+otdu/s/5sUb3MZ/dx/Nf9TYefCSj3HXB1HuO/fMrX3f7H/3xLce/tHMkbzFeTET2mhg65Tz9ustj9z6bx1zwPGotme8dp9kYw9mCEKoUsgrOOLwq4xNbmZjaRr+3iPd11Ga3DbKsQSMv0AA2gYKqPhljFQE/YwBD8FUi/EQFCTExvDdilunkC2ig35uj3++Q5U0azUlCiA5XQxTJdC4nAJlbSlF8qNMwTiX4Lr7uY1LvAKlz0FkXpxn7MkYwJk6mtdZiDBhbRMPPWsNx8yImOj4xBBEylxHEErSms3Acm+Spg68Ql2FNjEpQaDUbBB+o65osz+N1qethT0B0QjECCqFGtI4Ri514rEjjB5tF44D3czfcfNcNLCweZfvmC/utov32qq5uqUP1DEEbiTq9R+F7UT6kqgcH6P+5dgDGGPr9krquuODiC/B1tTSU59w/CuAXhxv9sAlPSreSAiznde6valTB2XrpxUMW1PkI/xW+P1N9UxiyGhQNBjWxVz9IQNRg0E9Uyo8e3jZ9U1YFvu8v/oVHfv4umqO7+Lzc9V3vv/FPXnHT8S/tzJ1jc3sTQRWvHiOWuf5xrFieddkPsXfqanr1PL2qG0t7IdYSjJi4GwsEFQrXpNfrUFa9Ya3c+4APHqOK9xHtr32NI+bxZV3hbJaMX/FVL+62gNcIrg0mDMeN0cV83dmE0Ee036jS6xxNi0zIi8Yw18+c4H0s1xkby3qIoy770YlJbCEmxN04cxmVr2MObvKkcaBYY2K5DY3phTf4upv4HpY8zzBGqasyAZuKBE/ZrwheUB97ElzWWMpTxSAE6ipVOGzEQ2ofhhGrEZvWF+nzTao4eELdBdveq3bkLW07+s+hPPKzt9z1+bvvPngHj7v6aVy85+F/3+3NfbLbW/xrhKelSGCvEj4p8F1EAZLzYgvtkRb77z7AgXvuZdeeHeeXF6BrKfiwWpZJln/+uXdD5WKXyV07mThPuX/Kcn7ZBv27XFUGHP44DC827uQMavv+1b7IHn/0wl037b3xbp77O3/GMz9/gsWpyU3v6r7vza++9TffetOJL+2cak4ylk/gk+SUEcNM9whN1+Jbrvxxdk08iLneUcq6jyBkNqOsuinEz5DEwiurilZrlOmpbThXIKkzLpJnPCGUCbyLN6X2FZoQdyMWUYkDMI1LebXBGpvEXlN5MKUG1hbR0QXFihtGFNa4yHw0lrwxijEJzfdVAgAj3z9zrRSyW5DYGizLFkdksZWAJ2+MkOfNePXFIRLrylVVUuRZNNigWCMpjLexVyBFnHmWk7ksVgaMwYcIckaj7+GyBi7LqTWConmW4YNS5DkuK7Auw1qT0hqXuhklRQgVYvIoEqMlrtj5rVnz4i+OjO5+YVnO8sFP/wMf/NTfYW1+5/jo9NO9978TSOBjCEUgvAvhJ89XZD7AAm698ZYk922W9HLO8WOlWS+dUEyodA1Hh3ONSsSSFGy55KLzsvunb/vHTvXnB+zVbPV10NQSK+a/LW7a8lrT6/Og172Bp7z/RiY6nmu3u+e+/e7X/vndc7fuGilaNG0bPyChEMthJ7pHmWpt4TmX/yitfIyun8UI1MEnUQ6lkTUBwaunKvu0Gm2mxlqUIerctVtTtFoTzJw4MKzpe1+nUNbGsJ7YyGMllgA11Wdl2cyCQU6saKoyWEKoCFphrMXYBhHjiq291jXJrItMvP5i6tgTkBwrgq8r6jq2/YrJQH0UAxEXKUsh7uRWDc3mCHVdLrUFY1BSy6/GikEc4e0pCsG5Apu3qKoudd2nVw+ioDyClmLIinFM1cP358HlqXfBR90BDDbP0LqKEgBimJ4coV/VzM4vUtU9Mie02iNUwSZOAYS6giA0my2qchGx+WSQXa9vtNzzxR/5iVvu/PjMbKfDI698Elun9/zK3OLxD9e+fJOqbkoYwJ8Dm0Tkd851tW7AC9h/9wH233OAXYkXcH7ZgTLEwQSwj7nk6REEXCnMcU5BQJFY95/ctZ2dZ5j7nwwDqI3BG3m9EVnXWy+h/XrIizzTb9n5rvmFo1zxl2/gYW97H81tl/JZc8sfvGXfq155ojw6Nt2Yxkm2XIYKI4bjnaNsHtnOt131YqzkzPeOktmcMKiHa0ggm0WMHYpAiBhajQbdXo+5+RnquoevSzSUieSzFMYaa3EuR4wbVgGszTHWUhRtbFak0D3m24Pr6wdKOUR2Xpa3abWnUYl5syTQTKxJYFnMbePnRcaetYLLm8PdyKQIYHANrc1wWY6xcYcffnaI7cMk1R7rigixJudps5izC0oINWXVJ8uLyBSs+8nhFDH6sYorRvG+jhUHwIhLn1nHqMYKZVliBKqqRk0W01dfomrwAayLFOk4il0IIQKsVgA8npGrxIy8oJHzxeMz9+679e7PM9aeZsvkrttDCG8p6/6TVXR7rBLo04NqM2j49yjPH84aA1gPC7jwkguioErahM/hoxDhFxMYuGyTp7SPveTpL00gwfK5gKXAy9PPsw7+VZVQ1ex9+Jkh/yd1ABH8e7sIL2CZktLyocfpO3xeRZ4pey+94ejH3gq/9kKuuq9BtfuCK9977G3//JnjH3xBw+ZM5JMrDH9o/N2jbGpv49sf/GIsQqeaIXNNah9BodgLT5rYGpZ2aRPr5AvdPs45bGrgqfqdRCPVoZOISjwNjMlxWSsyJhIFtmiO0WiPY12T2leYBJ4NcmCRWBWIwhxxHkBV9ynLDtY1MMYMJb8Gi9faIgFadXwvk0VQUD3GLOWEYh0guLyISkMpUh04FZc18aEmhJIsy7FZEc/FZpEKoJBlBf2yg68jbuEyl6oYfWxWRGkyX6NiUZuDjwxGJaoUaeopcC4SnBClV0Z2oKaqQhQeqcH3E5i4BNiJxLTA+6jxL9QgxSRu+r828rznq6Mfu+Pem5lfPMqebZfNGuP+uqx6VwhclSKBJ4JOofqeIRDIuRknluUZx44cZ3rzJjZtmaZ/7nkBBcgvIlKsyACE0umK+n+CAc8pGUAoOwtM7tzB+PZzm/unS/+eTPXZMuSPmqH+kiYFTjT8s280v8vtuai+731v5q6XfT9XtC/kronZ77nu8Htfd6R3qDWZTUYEXP0q47cc7x5hU2s733blT2FCoE9FkY/RrxZjKU8j4m6tI2gqi0lU7LHOxn8jeB/RdSOGYGIoHxemS0h/DkC/nMfaLO0QUdbL5U2cy2k1cmrfpbt4IqLmEqsDIoa69rGSIDamFFpjjCPL8lhSRCLxU0Psxgs11pokez1OVXvKqkeeF9S+Twg1GBudRqgpez0CQl6MMjIxRnfhCEErvO9DqLEuw1iH+qQelEhPeZ7jXBNrhLruopLR63WofU3RaFAHkrEGpK7RqkNQiZ2LYhGtIhGKWLLEGBwuEpJ8jS978XuKkGUZmIK630OIlYHgIyAb0tBQZy0Bj2hFrZYgW3+vMeIeVVT3/tfb7v74QlWXPO6a5/tm0X5Bp7twW0D/Z2oz/jlFs9RMdE6xAE1YwK49O4cO77yyAJNtuhXlv/OA/w9KUlsuueh8cP7/3YbwDDOYbjr4GQbahgH1/jVmYvPPML6JW/7lDdzw+z9MkWUsTLZfduPx975UJbCp2DTUlV9t/DP9I4wVkzzvsh+jYZuc6B+kyEYjSKcBgwwJONGkTcqDlTxziXgUhTBiw4knEvpkyKJTVVxWUFYlqh5nG6jWKZ2IzoXgcWKZmzuB1mVkE6qPRCGN2IHLbCIOhdQVGLkBvuyBJIKRMmT0GRd3UlVLkTcxtoo9AL6KIKOLXIaqLpNqUzQeDTW+iuh+XXlclqew30dHJhHoC6lEWGQFzfYUom2s8cwtRAGR0PexouH7BJEI8HlPCAaLgqQSnMkQ6WNF01o1BCvJMXmsy1L7dMJINJKEqqomeB81EVLiG4InGIMQnb2GKo4LN9PfRd548NhI+KF99335s5gWj7ziCRRF+391+vMHUH1VWiM/nSQkfvKcVwTuOsD+uw+w54KdzJ/jHoF1JgAiKE7OZx+ACOXiIpM7zvXuLwDvc0GfESR2ydkgxFF3ZlnDUf2brekdv9F1GR//nRdw94f/kaJZtG179M33zd71vMIVWNySntyqsH+hf4KmG+U7r/oZWrbJTP8IuW3i616i6WbLQhFPozkFInQ6x+Oub3Lq1FkWgTETWXsSPX5IEUDQgFVLqzmSuAEmAm52ICiulP1FfN2nqsroUiQi/2Kb0YglhsikdGBAdRUTiT8kDQAJEKiHDihowFpLp7+I97E0WVcpckEi0YfYeGSzAqMx/ar6czSa49ispN+bw4gDiVUSxOJcA08MlaugFP0ZTGaxrk1mFuOostY4ZdmLlOIQCAGwJrYwhxoxYF2TEDxFw1LXfaqqSjt5NOgsi4SmuqoiuUgVg1D7iG/gGeIjItDIC4JGjcLgQ6ygGED74EYvN+4hnx63d/zA/oN3vrnq3cuDLn4UY+OXvXpx8ch9quHtyWH/BIJB+fEVhbWzWNomJeu33HAz23dti7qM50MmbJASp/8xMeyXlfv/OUo9BrnX8t3/3JyAvtuqPtNFHb6oxKtgw0CiO2A1/NzYpt2/Ya3j3//3C7j9w/9Ic7R9SdYa+YyG8LxMcuI+o+t8hNCrO9R4nn3xDzHd2MJ8GYkqvq4S2SalTEaGLiekdtEIhgm9so7RtrFY43CmiNz8gVgG0UkIBiOKS3pxEeBzQ504I4JRqMs+RhLYFzxisljvFk1lRxfLYdhUCzeYVIbU4Ane4zJHuz1JXrQTWBkxAUJ0CD6E2ENAHJ1tJEYvzkVUP4hJAqFCVXbxVexzcM4mw88iyOa78fMF+mWPhV6XzsIcx44eoO8rGq12qtsHqjpGRbH7T8mcSUrFYAiEuqSqY0SUuYyiOYbLR2K0IxZJ2I/L8ui0VGNVQkEG56AJK7EG72MHZZ5lUXfApFRKeyhGNNv79832zv9RVbPcdse/cOLEHeT5+DtQfZIGLaMWgv4YGv5kBZF8MKHoDB5BA0Wj4Njho8zPzpPl2blnBw+YsMuclRPVNZNDz1UEUPW6TO3Zxdj27fQ7nbQLneF31+HP/2fgWyQ1dpIYbwPDlaCoyH9tbt79tzOdGd71ip/k9s9+gKmpiSch9p9UdfLk1yiW7xarDs+66Lu5YPxyjizehzON2ERjCzCRfiq6RK3VUNPrzsU8NG/Rao7RLxeo6jLNnSuQRLG1JsO4Al/3E9vP4BU63S7WQLM9jffgo84+PtRkWSMCiAMdPWNjWK5RCQjjE7fAJ0EQm3QD/FCjPnMWsXnkE0iVpLHAWkOetyM4qYrLHISaEGryXPAhTTQKVUpnMqxqFAcB8kYbxccS3UCqvOpH4zOWsurT9TGPVxGyDHzdizTmvBVLi2IYGc3pl30QmyIAj68rmq3ROJMvKHkeOQFVWZG7bAjEFaGmqkt8UAQPJsbpIUmguyzHe09ZVgPYNQ2TMXgfYnVAIdSLMY1z07/rrN8meuvP33HXv3HxRc9hpLXtowudE49XwodUdSSovkQ1nPDe/2YIdSpXnl2mvjC3wB233c5jHv8Y+r1zyMQf4HurDN0NjHKw8+g5dDe+rhmbniY3Bu/9GYX/uuynirzaqH6voARJExeG316RmOA+f3L3le/61Mfewttf/fNU87NMTU3+AGLedGrIijDTn+GaLY/lIZuewEz3MJkrMCaj9n3EupR79iMAKJFxJxjyLBpdAELi8YuYiNiHGjRSV8XmUZJbKgayWbEJJn5+Vfaj4Gei/lpjkiiHLhk+inGORqqta4iCF3FX18QcjMScgXCIiMHXfXzVjc0+YuLwEqJjCgpFY4RG0aBXLkLZZXqijQ+BA4eO4azBSCAk7KLZamOsY7GzQJblNJpjlFUFWoNYfO1xWSxrGmvJshwhlkiz5iidhePMzx+h352n012gqrrMzR6k7HfTPIIQZxbaaKQxjYqfnedN8ryJsRmN1hiNYgSxBY3GCC7LIQT6VQ9SOtGrythebWzUI/Q1wceJv86YJLCiOJdhUPq+y1y56SXk2dYRe8f3333gw0xOPoYtU7s+t9A5/rgQ/AcU3axBfwPlMPAaTbMK5MxtlCzPuOn6m7j8ystoNBuU59IJLMX/wy/plnZ8OXe7v0BdVrTGJ5jatIWw2MGFMyMXDV7hDb+q8NODb2lTTifDVncFY545vevy93/yY2/l7//wR5AAo9PTPw/88akYvxHDif5Rdo7u4cl7vpW5/gm8BnIVyv4ixmRoqBJgGOtbXhPoZWKJLwJ7Nd2ygzFZjBhSLmzEDpHrqHAjoAbnCtA4wkuMwVdl6h40CH4YohrjUrQWh5FoCNSJwgsmaf1Fpp/YDBID0CTdAIxg1KIyUBCS6FxQfFUh1iUEOhpGkecEzSnLLs1GRu191PFLaLqmNMOKIc+bQ1kyrzUaKlzeYGRknMw1qEPF3NwRZo7fy5Gjd9PpzDA3e5CZmXvp9zr4OpySgejyoTmDqNCAc5Ysb1EUYzTaY7RHNlE0xtm6dSdZNoEsLlJX/ThXoYqgpqSejqCD1uuosmTzDBMCKl3my/HvM83LNje5+zk33PrB2lz2VKYmtl83v3j88SGEj6vqZtBXg+wXkXfKWdpQo9ngxPET3HT9zTzmCY+mLMtztCvr0nAwsxRVu/VhwrP3BFXZZ+ell9CeGKe7sHDG75fO/YUCv2VVhx5WEwojMqD4mWds3nX5Bz7+8bfxN3/4IzgrjE5O/1YI4VdPzWcZOtUiDdvkWXu/F1FBRclMTr/qpLFaLopaJrZbIOBMli6tJuNQRBzOJmqnxEzWyCAMMzgDXtPOb02abhsJPIMczZhUARCTZic4xA52KiLyneS0BjTeyAmQqPGfNWhkOSrQq/qxBCmxRDrIua3Y1NgUnY1xsRxZ+TJWUjXEcl2ocS46AABn8wTEBbqdDtY5yjKKkapAuzVOozGKSsV9B27lwP4bOXLkTo4cuYPO4twKX+yyCOTleWobTqvz/oJF1aXVMWiAqsoOve48J44fiDu7M9zVHKU1sonx8S20R7fQHtuM2IzQX6TqL9Cv66hfSBznpWLolxXOxLm/QXvM9VrP6NldH8vc7U+76bb/6Fx20RMZG91yW6cz8wTQTwcNE6r6T0SZuS+fbUWgKAruuO0OHvKIh2Cdw9f+XNj/2hQ/Bp0DAHBVfnAWh69r2uPjbN61i6rfP9u3ewTw+gEGYBO4OMj6ibPpvnnrzss/8NGPv42/fPl/w1rD6PjUn4QQXnLq18fTqbt8y4Xfy7b2bu5b3E8jkXFym1MPy21uKBRBqqmbgdIlJhFoTJLo0pRvD4xNUxtyFLNwaSSXDx6TWIAhVTOHu/3AGEwkxNR+QO+y5EUj7WA+ISKRi6ASO/msCNYZ+l7I8iJWEKzQKIoIyKYmnqBCVhS4NAqs9hVBDFWosTZ9Rx9dr3Gxk7DRjOh8WcYBIM3mKM3mGP2yw8H7buOe/ddzYP91HLzv1iGabZ1QNBpDfGK1IS93+adTBx9UNIyxuHxQztLkwBZYXJjl8H23k+eOkbFtjE3vZnpqJ2Mjk2R1Rb+/SPCpUmAEG2RYGjfGQOhR+dajTXbxx03/1sfdeNuHu5df/AQmR7fcOteZfbJq+ByqmaLv1xAurX2YPZttWwwcOXyYw/cd5oKLL2B2Zvbsq2eyXjlQUhlwNfHnLD5MEPq9Plv27KE9PkZnfv5s+pwngfcuXyIy9GQ6CGO/f+eOy97zkU+8jVe94sexzjA2dnrGb8RwrHeUK6cewlXTj+Z47xANG/nxVSrLWeOG+afXkHT8ImAYEeRYt44k61iWy21OXrTJspxevxPz7ITEj7RbzC92Urtw3FVVBNGQavMB1GCsYGwMt33QGBVgMaLUZZpIG8cAJwmtASLt6YUK+ooBbKOFGIlMuIQlxKgjnr+1LubuafgXWmNcjGhqX6dWXxObmwS8j1OCJie30WiMMDt7kNtu+zR33PEZ9t31xeQwoGhEduHwrqmu+HnOD5Vly9iSF00i/w2Cr5k7cYATR/dzb+aYmNrF1KYLGJ/YTtYYp9ebQ/A0Wk0WFxeH+XIkclUEbT3UFpd81C/e+vjZ41/s5/Yasmzyy/3+/DcF1Q8KulmVfwV9/NmcgrWWqqr43Gc+x+49uyJx6RxU0HSNB1DcetC/nMXNiQBNzsT0NKGqlkUXZ3T8i8CUDl2LrqgKGOTF23Zd9uaPfOod/PErfgIXjf/lp2P8gtCpFhnJRnj89ufQ9/1I60Uo696QXGRwaQ6l4CQb5lRGBj3yg9DVJFHLGMa38gKXt+lXfXwZEoiXQMGktzfg3AcNSYJ7SQkoUmCjOIaxDpvGVMcoyA+FQowYJKUoJKZKqCOxJ5JtotGLRknvIm/hspx+uYAPAWtyfIiAkzUxHQgapdqNWNRAltlYXycwPraVojnG/ntv5uMffzO33/Ep5ueOD42+0WwtC8/Pag2ccm4q69e8hp9tjKXRHIlRqq84duQujh65i5HRKTZtvoDJqb20R6ewUiG6EDshnaHX75FZi9GKOow83DUv+WgjO/TkW+78aHfbtkcwOb7jQ53OzAu8hv+n6OMiIKg/rWd6zgqNVsHBe+/l6NGjTE5N0j/bSHo5djIAUyITcAkSXKoCyBnfMF/XNMdGGJ2apKqqs/nKfwY8dulW6qqyoP7vqaltr/rIJ97OH73qZwY7/8tCCL9wutBIx3d5zq7vYXNzO8f7h3G2gQ8VjWI8kpnKheE02tw6ao1hfux+i7X4gYR13KUj/8H7moXOPKbsJnYdiaNf0y09ucuGajqVr9Nub/A+LtZB776YCNbFklWNtTaJiYNxUWzT2vh8o1H5KDIEBbE2cvk1QIgMuaCp3ZgQx4UbExuUNEpdW2Op6hrnLNZlqbbt0aCMj22m0Wiz7+5r+dzn/4Wbb/5obGO20Gq3kvM6W468nMFfTu2JA6M01tFsj6Kq9Lqz3Hn75zl4741s3nIhk9MX0mpNQKioqm4s8yJgLIWp6VYjjzzedx/oV8cff/f+jyk8jvGRHW/pdE/sUg1/pKo/FVQ/o+pfe6Y5tTGGE8dPcGD/AXbt3UO31zu7NEBWBfoS0xy3JLywXCvszPiAglCVJVunpsgbDXqdzpl+3RcAL17vfirgg3/Dlqkdv7qwOMurX/vfCapMjk2/NITw0tMruwgz/WNcMn4lD51+DAvVidhHHsqYT5qM2pdJyLOgChV16vjT4QiRCEeGpLBjbCSgxLl6QukrqJIGXuo1MNaR25inq0jS0E/AmhqMiYYZXxMVd1VM2sHTwE8bR30jmrgLAWcsmrAHRVOrb+yvDyFxF4bOPiRyjZDlOYSSBDfG37mMOkUpoIyOTtFuT3Hg3lv51GfexvXXfQiALLe0itYKEc3/bCM/HcAtnkOLvIC66nP3vhu47+Dt7Nh+Gdu2X4bLR6DuUJWxwSjqG3Q4sdh4bJZd8m7jb/qWO/d9mF07HstIa/PLfd3dG1R/DvSvgU8FDdefMTvQCPvuvIuHP/oRUT34rJmBS1a0JAgiCURZMbVczrDrL4JQY1PTZ7MD7ADetNEfQ/Afmxrf/MOK8qev+2U63UUmJ6b+SwjhZaddqfAluSl43NZn4IOnCjWiksZtC/1+rE3Hzjsfm280km/QEHEBsQM9uUT/dUOG3WB2vVgznIKcuyxxLhL7DIOYjHZrhDw5AZHYtksCt+LDYF2GazSRrJEqCJELYazBJCcQ0FRBMCkfDnhfps5ByJwlz+K9Hh9v02gUSy2oxpK5ROlNyjtF3mDb1kupa8+//tureO3f/CzXX/ch8sIxMjoaR3klpaD1F9z6j5Oto1N8i7MwgrVvpArWNWiPjGNE2Lfvy1z7pfdw6L6bMaagKEaxYqhDYGJ0jFZeI2bqmyW79LWg3Hvwi3GgqslfEtS/P/EK3ueDzwdDRk730Wo3uf7667jphhsZHR09RwlUrJzpkAkoK3fEQdvQ6eYvglD2emzasZ3p7dsoe70z/Y7/SExh1/PYR1uNkedVdcXv/8WL+cwX/4OpqcmniMjrTtfhiBjmynketeWJ7Bm5iBPliWT4mnZ3HU7EDRowYpOCTh2XrxGsjch/5WusdVib4dHhLhwFLAdDOBP7DAv44eQbZw3WCd1+lO7KXITyBqejqhhnEZfhXBbnzFtLvzao1lgbd3aXWVRiiVBS+TD4ONEn+AqfxD0gUnatVXq9MgmlhJh6hKQv6HKK3LF16x7KsuZzX3gXH/7I65mfP4HLLI1GK0YU9zPY8oHayc/0DWW9RDlpO2TtyH/Yd+fnOXb0bi666Bq2bN7D8ZljSYgUoEcVpn6k0bzqrqa977fmZm9iZOwqnCm+raq6Nwd0p8AbVPUFZ1YNMPR7PfbdcSdXX/2Qs0rNl3zfMr7PMAVgdQHgTD4kqtqMTk1hnSN0u2eSs7yMOKhh3XBNRL5l787LZ/7sDf9jYPwXiJh/PZNoo1d3GC/GeOTmJ9H33ahDn6S2vCpjzQZl7amDDKf0gmCsTWq6MeePLEDSyKuwNPwSieO5jIllPZtH5Z/E1iP1SQQErSt8UNQVqcc+zZGXmOuTUPrcRCA05vVCtx/r+SbN/DNJLSg28ETsIE9gniVhBMam218PdfXyPMe6+J2r2uNcwbate7jzri/xT+/6M+7Zfx0YaI+MpihMl7n9r1wDP+OPltgfkedNsrxJp3uc62/4IMe3XcbWrZfTqwZdrmBNTS2bflNsfXvVu/VNh3oV01MPWlSRbyKE6xX93hDCO1X1jWfyVVrNJvvu2sfcwhwud6np6cyv2pBIlS6MGQIEq0CC0w3/fV0zOjHBxKZN9M8MsHgC8NKTVBd+fvuWvZ/51w+/kXf9++tpj7QKEfMfqto8/fsrLFYdHjL1GDYVW1isFtJsvojI587Rr5QQZChskTs3lKiOTWt5oiKRRl0nhV+TpYm4krj6sXHGmjiVV4YKuStpzjaNzRpECy6zFEUDm4DC6FA0DdtMCK416f2iIQ7aYc1Ai19M5PWn7kMjQuWriHHYSCnOsiwO8UgLY9eOSxgf38Q/vuuVvPLPf5J79l9Hs9Wi3Rpl4AnXjcIfkFB9/Tc8o48+mYTOwAmkpzYaYzjXYN89N3LjTR+i1znOyMhkGqHmIVQsVpvf6GXrw3vdu5lf3IeYxg2K/mAIAVRfp+q3htT0czoPmzsOHjrI3Xfto91qLUvRT/O/Qb/PCvuWtaKgZ2T+EiWaJrduoz0+Hsc7nSbeAfzDhpWF4N+1c9vFr7j2po/zsle+iLLu0my03qGqe84EqOzWHaYbU1wz/Ri6voMxDps69oL3qTvPDst5kgxI04V0xg6bUJzNYrUg8hKxaYAmEpmEYgwq4H0vOQmTWmYjWh/DdUHFItYmgQ5L8BKVeURTXT5QE0d+9EOgrOv0mekCuiyRgOLkHpdFVaBOLzbIWGeHrROaVBKNjTtdVZd4VbZsvpD9997E//2bF/ORj76RLHOMjo7H7zsgPn0tGfkpfjNNeM/YyARVucitt36Mu+/+MpkraBUjGBObqharvf820to9Md04SsYRFPd3qvpKr8Gq8jZN+g2n8zDGsDA/x2233hKd9Zl25646uQEnwMmaPuAzrDEiuOyMWxj/DNi+Qeh/otUY+b6FxRn+9u1/gCqMj039dgjhm8902XWqLo/d8lQ2N7ZytH8UK3HIpjE5edai1jjwIhpqDJO9hmEojgj1IMwWYi5uDNaZ1HhjyV1E5L33YARP7AWQxP4jNQB5H7DOYKxL5bZIG1ZRqrpKij8MhULrECmulvgdCBInyySn4kz8e+1D5MdbN2QJCpIiiDhzz1mHGGWsMc3E1E4+8cm38rZ//n2897RH2kOW4dmF8Oc/XD+n30zkJNi5khdRs+Hee29kcfEYF+59BM3GGP3eHEi+KciOfzLse4pW+9FwIWB+FtVnBQ1PAP1J4C9OJ8VWDbRHWtx15x3Mzs3hnBuCtmdeDlw6R6MrMn5dChNO4xGCJ28WjE9PU5en3b30CDaQWEp5//ft3Xn54j+859XccMsXmJ6efn4I4VfO9Pz7ocd4Mc4VY1ezUM0jomkgRQyIRse3UORtCH7AKI2af0kcs06OoZE63FClqnvpb5ElCHEOAJjYcppC8oH4R1TjyZPefhTQiPu7Js8fIsgkdhj+R2ZglNcejK2G6Dxs6mYj4Q9BI7aAib0G1kYG4EC0IsuiVLhXz9TkTkZGN/G2f/xd3vKOl6HiGR0bT5+p53Qnl//knXz4GE5ROv330ySg0m5PMr9wjJtu+QgL84cpijHEVNShePK+mUtfds/xJp3O0TSngOdGajKv8j5MBR84nYdzGQfvO8hdd9xBu90+M7mwDS6yWfJwsiJvPx0fEOqaielNNEdG0kI7reMNJ/nSr5ue2Pred33wb3n3B95Ee6S1BXjLmTs/YaFc5MqJh7JzZA9l6CWtfTvc7edmD1GWnTgmS5eXRQ3W2sTpt/g0kYYkNinGYUyGsZH7XwePD1UUu0zCFZHUY5bAs8FEHZFU04/lmTAYgGFArGCcHZJ/fHJMxgjOOYyT1CmYnIyRZPQWsVD5PmVV47JsWB6Mz7Hs2nE5Vd3jVf/3J/joJ/6BRrOg1RzdYOb92Rs558HI5TwYOctev95jsFm2muOE4Lnl9k9x7NidjDbHIh4U3EuNZN/U6/dZ7MyBcDvIz4UQrKJv8KqczsMYw9zcLLfcfDNZnqe08XRTgNUDQAdlwKgLtIoRfOpNQSJQVyXNkVGyRoNqbu50AMCfA65ct96v4djk2KafmFuc4ZVv+J90ugtMT216RwihcaYOoNaadtbiwZMPo/T9aEQ2X5avC1XdjyU0EyflGrFxeHy6iDbJdGEcmQieFCmgsYyYdvlBqXBwIU3SAYzNQAGTOPzDMpwhOSMdjgFbWrNJ9Vdju3CkDiedfpae67VOVGKJzycCgrjoGDQs8UF377qCO+74HK994y8xO3eU9shIEhC5fxLYV2qofiZg8Jm/FiCqJ/m64u7911LVXXZsuxJnKspy7K1iit3BV7NV1UeM/TMlvECVb1HV5yi851Q/3asyMjrGbbfeypEjhymKIrYJnx7raRXImRzAOhoBp3WDg1fyRpPmyAh1WZ7ORZ0A/nCj0D93xQ82inb1J6/7JRYWFpianPrlEMLjz2b3ny/nuHziKi4cuZD5ep4s9e4bm2Fsk9r3kmKsWaLXSmzFVdGhgo4hoJoqBMZRBwUCdShjjd1F0Ynax3KeFSHPHLUKhTWUfjBMIyUe1g4ZgYMSYnQi4FUwEltW0diUpBIZhDaz1LVH1Ec+gUS2mGp0VD7p+4uJjTxBouDF9m0X89kv/At/+3f/I44ZHxsnyVxxVroQX5NGfv8AmMti5Lf/vlspyy4X7X04MDba78//ozXytLJf4+JUvu9S1YOgf6eqU3qa36XTWaTb7dJqtc7oZGQdMNCIrBwXPNgNT/W/4CsaIy3GpqZOF/3/U+LwnvVWw7smxzf92/s//g985FPvYXR05HIR+f2zWQiJXsNVEw+h4RpYa3Auo8gcViTOtF+2EJ3NIjBoi2EXoB2M6UrjuaoQogSVSSG3sSkaSGF2mv4rItTJAZdVnPWXZVHWOmu0kkyWpdGMM/8wEieGicXYaMC1j0KdGKjrKPNlrWAsOBcjCytuSRM1has2pQ8aAnlWsHfvlfzHx97Ea9/wSwT1jI5NJJ18/arLx089XD/9U5GTUBKX3nt5hGdpt8Y4dmI/t9zxKSBQFM2n1qHxC0qbqg++Lg+phpd41UkN+r9PhxXoMsehQwe54frrGRkbO7UTWPFY59xXYgDLUMJTfgh1XTE+vZmsaEQJ5lM7Hgy8cIPQv54Ynf6xmblj/N+3vIwstxRZ4+/Ptn2053tsa27nwdNX0/UdMpfTKho08ia5y6M6is0RcRTWYk3sk1/S1x9w/IEQB2cMJuiS+v7jEM84G6Cuo8ClmOhQvfeUVY1YS5YVcT6edYlUFA28qqMmQHzfKAhik+a/dQkD0BDn6IlQ1lFp2FqX0omEMRhNY8KjqIURodEcYfuOS/ind/0Z/+9tLyPLDKMj4+h6+f7XkJEvP53Vp3ayM1ht5Ou9buW/4zDXVnOcubkj3L7vszSyjLHW6MtBL8jzKawZIWj1p6jeohr+l6ITy3soTvYA8N6zsDAfHfZpRjaiG9Xfl+7SMmbXqRcXxRhcnp9uuPaqjf7grPst5/JDf/WW32JufpbRkfGXBA0PO7tClNCpuuwduYhJt4mAp1U0aTTGyPMRMlck449heMCn0NkAfphLmwQYio2jsp01Q2Zg5OFLytElGbcM5/1Z68hyh7GO0tdUIWrQ1XU/imK6fCgqqYN4RWLxLoqEmLTTGIyLffmxW89RhxjhiI0+vXAZzphILhLBZTnbtl3MO//1T3nnv/4peREFPKIoyNfWTr6e/1oZ0q/cydfu7Bsb+dq/rf8dW60x5haOc+Ptn2bL5jGmxyfebKSg2diFkYIQqm/XSCr766CeU3n4UNNsN7nxhuuYm50lz7LTwwDNetdkORFITj/l8t5TNFuMT286nfLf04Anb7D7H9g8vfO3P/flD/HhT/4ro6Mjm0TkD882D/TEfP5B4w9GtcJZg3NNjMnjGC6XpYk5UUXHB6H2MR+34oYt0sYINstptifJ81YC9DSBciSZLE9ILbmYiOBjBDVRZjuoTyIiIQl9RHVaESIfwMRGIGvNkjNKhB1NmvuKDmcN+BBS41EsBTpnscZSNAqKZoYrGmzfcTn/+K4/5N3veTWNZkFRtIeagA+kkZ/MwM/GyNfm7esb+XID3+h9Tmbkq6ODk53HSGuMmbljfPLzH6L21WOaDfMzzhmcHUE13BhUX6chfKeqXnaqE37zPOfeAwe47bZbaY+OLAl8nAYRSJeR9yL+r8t3/tNLASIA4mKTiZ6yYskfb/QHa9xPCfCuD/0tAEXe+BtVdWfrAPq+z87mLnY2dzFfLUQZhFCioQL1y8LgpJ5rEv1WLMYWQ/aeDrkJcXfW1NGXmgFS3m8xLjqG2NGX2ngFKh/wqsNmHRXIcocYIQhxvJV1qRwo0ZitjdfXxp0fIZb/bHQsmEgEsul3IoqXWD7COHbvuoJ3v+dP+Zd/+wuKZkGWNRNn4D+rfHZujXx947w/o17+NzkjI7+/81FVRtpjHDp6kBtv+zxo+Yqb7/jkli/e8GnKSsmc+wmN9NLXhxDFSO/vYY1ldmaWG667jizLGTScntJjA76fETlDsFWEquwxNjlF0Wqfav3/+cBD192lg//E7u0Xvesjn/4XPv/ljzO9aepJ3vvncQ6Oru+yq7Wb8WyCWqN+fNnv4ssFfLVIXfdj+D1whhK74pwBa+MOX6cRZ0E93d4i/bKLsTHPjmq4qa3CRJFLYywhlXDiEx1qTJQVT87CqxLSuCs1abavicY/cDDWuSRnbYaL26uCFYzLonYg6SOswzgHNjYw7dh+OR/4j9fyT+96RYwIsiak8/haMfL18/EHxsjvv/KmjIyMcWLuCHftv9Ftmdz6ms7iHPcc2AdKJSIv0RAeo6qPPBUcIITA+MQEN994I4cOHSLPi9OssshQ32HwO6OyKuw7xZMOwZMXTcamN6Gnrlf2OxtdqHZz9BdOzBzhDW//E1BoNlp/cS6Mf9DWfNHIJWQmx4ca70v6dUW/KumXfcq6TgQbM2TfibUUzfHUGRYSW3Bw8SIIqBqNdTCdR1xGQIZhuXFZ1NAj5nCaVIBDytddUaCSxWjBxHkCNos8fpu5qACkAyJvUkC2JkYDadCDSbs/aYQYJtKW9uy5kk9/7h9505t/iyzPaDRaiYL8tWXkG+fjpx6un9dDodkY4d4j+/C+/50X7r7k8XMzJzh2/ChO7J8qzAfCG08VC0CUhc483e5iJIuderi+UvYPUol7+W+MnHJ5QRVsltEaGcVXp1T+ezZw9Qbu6QNbpnd86g3/9MfsP3Qb7ZGRH+52u1eeixtUhZLNjc3sbu5lrpxBsBjbQDH0a0/lPT4MhCp9ugQWVZKQp8fayPP3aeSXJkHSgXGKDK5kSFtxkUJ8QZNWv8uzFO4z1OXP8oxGs4g4QdrtKx/f0xVZVP1JqcEg3B+0ES6x/mza+S3iLMF7du66jJtv+xR/8dc/H6W6WiPrikqeD2T9gTby872Ln4sjKiwV3LrvekZGR/9qy5btHD1xNCouwQ9p0MtV9emnEgVkWcahQ4f4wuc+y8joSBoLf/+Plca/NAnMnCm7K/iakbFxrMsGQ7ru7/jdjf4w0hr/77fccx0fvvZdNMcaWbNo/PG5Uo3t+R47GrtpmzZVqIYAWkjDISObbnBZlur2cUpufxhDRK2/gex3XKVxfr0fdgmSuvtEfNqJI19fnFui/KZhFAp0Ol16ZS9GBs6CTVOExdCvPGoEcVl0GAhYmyiFoEYgAYVR/isKjW7aspejx+/hz17zk2hQxsamhhOavzqR9a8OI7+/w5k4dOXg4XseZE32o/1eyYm5GYyRfwa9V1X/Mkq0n/xBUt021lE0GmctE2YGoJ+K3A8EvPLh64r2+Dh5o3kq+f9jgY1KeR8ZHZn8wsc++U4WD80wlo//f4Ewda4uvFfP1mILTdeITT3EIZSgZKm2PhjRHYjqOnGMF0u5vEap5iidlmww1ffNoFyYVHljjh6GY8Kti7+rfRz+oSZGBSZz2NzGlEEEr3Gnz1uNWFY1Fps5Gq2CrMgYGWvFYSAi2DyLgKGx2Cw6Dq+e6c3byZsZr/mLn6GzOMvE5NRJVXvOBllfaZz3h6zLWSPrXw1Gfn+pqLUZVd2nrLt/aIyxR44cjqPURF4QVC9WDY+7P30Ar57mSJNbb7uFo0eOkOXF6ZUCZI0DkOHgwNPpCDaJXRbCKc38+72N/pA3W//rjvtuYu7Lt/Ht257dzDX79X4oz5nxZyZja7EtFUJ1GSEisvtkOOQjTXdNo7edMUtadxI7/aLaruADQ0kva20s32VxVy+9B+Mw1lCrUifHIi429ohJZJ1lu7pJ+n+advi8yHF5Tq1C7T0ySD9ECYOSYmYjaSiBikWzzaYtu/jb1/8S99xzE+OTk1Gt6Dwh62vD85OBbnxdGPkp2Y1YQvCTzWbjL5xzzC3MISIfBa5H9dVD+amTPJqNBrfcfBOzMzO4LDsdQswaZWAjq/46KAqe7L8QPM32CBObt55K/f8K4Ckb/O1GbWUfKb94F8+Yvwpy8+v31gebuWTnzAGMuFF2NndSap/MFENRjBA8tSfNtXeAwVmbpg/EerzXxLQzJklDRypgLLfF5wSESqMcOAIuc7EDkKTlN8ACMIQ0egprqVPLrtgk0JioxMErwYfYdOQstUJZBXqlj6XBYkn4I4hSJfLQ9u2X8ra3/x8++Yl3MTI2ulz+7bRr5MPFmhqiVhhoQpGjEKoOxUUGkmkyGFGWrtmwS/HryMhPEWD9b9bap3Y7nTixCX5alWtUdff94QC+9uRZFsFmOXXjX+9XboVnWDYf4GSaBQJJOeeUPv2XNwyLcvu71X3HefxNE8yNXTz9R7N//9+N2LOZJLSq/t9jd3MPk/kUVRrQKZLKdmkEVxjm/B4NhsxZsIbSaxLTiI4AA1YcQUzU07duWNM3A8YeSb0nje92LrYNexQ1JqYMNpYAJQ1LzbIMm1m6PY9LNf06Wa/XmNs7K4m1l4zKQvCRJ4AEdu68nGuvey//+PaX02wWZC5Lu//y8VvrFwCW79qDuxuCp6pK+mUPX53dRBrjDI2iQZ4XEbVeVvn5ej6S1sXfee93dzodPzY6+hEf9E5V/RNV/a6TvTYvCvbfcw+f/vQneMH3/QDdbucUfcBw1NFw1oYbCAUO64Mi94sKBu8Zm5wmK/IhqWSDYwL44Q2uwOEwXryhde1B8sOLvDX/4P+6p3+vncqmOVdTZCqt2VpsITcNevXsUJlXiLV5mwZjBkgCHJEJOBilHdV90/QdjaF3llmqkMpyhtRixBDpFwOlJ3IIsgwIVFUVabq6pDOnccghHhMrEyYgFrKGo9ursUZS6TESisRIxAAlDqiwFoIGtm/bzczcYV7317+IEWgmxH+FtoNsXOYDqOuKTmeRulrCcorCMjUxxcT4ZiYmp5kYn2Z0dILxsUkazRbO5ZHZmEgqPtRUZZ+FxVnm5k4wPzfDzMxxjh0/zIkTR5k5cWJZ+mhot9sr6thfpw5huzHmtxcXF/+nsZZGUfx31fC2yAqRcDKAtuz3cS4jLxoDZ3J6tcmU9LuVoWH6s5zci9S1pzU+Qd5o0FlYONmHfx8bSHxj5PXSDzT3d7gnOzry3s6nfjoKbeo5Mf/Yn28YzyaSWt/gDONgD69KCCbp4sWR2mpiDq8h7q4+KgHEeXzWUaNUvkKcG0pDq8Tau0lNO4EYlgUx9OtyqXwHcdCHkVR7iSBh6QO11rG113u6/RI1UWo86hUYgqS2XrvEIHSZwdkGko/y+lf+FLMzx5jaNE1ISr/D6U+ruB0Dw+/1uiwuLA5/v2l6ml27L+WCvZdy4YVXMLVpJ1u27GTzlm20mqOpeSlWSTJnk6yYxrbmgbahhkhP9h5flXR7iywsnGDmxGH277+Lu+68hX1338bdd9/BvQf2o8wD0Gw1abXaX5eOQEReGnz4i4X5+X15lr1dlYMKvwj6BxtHD9AaaXPLLTdx9PAh8iw7NZmw4Qa4tMW6ISogq/NE3dC0rDWQPP/9eJ6XbHQGYaL1CnfzEXbeVvI2d/Mv39W7uzGZTZ2zCXJBA5nJmconSEU9VKPUNskrKT7twPEZgsGKRKBtMFNOYjtQCH4o4BlS3j2Q5x7k+JWvI0nHxjShVsFiEROiIlDiDwhZ7B0QcLlDg+I17oxeooNoNiK5p6zixJ9Ws6CqKoJGh1RWNTt2P4j3v+c1fPmLH2J8YgwNuu7OP8jD+/0e83PR6LIMrrjiai699KE86EGP5sKLr2Lbtr202mO4LKPX7xNChYaabr+P9rpDoRE0UNdxcrC1JrYnpz4HQdL8AXBFg63jF7JrzyU8/NFPpeqXVFWPI0fu5cA9t3Htlz7Dtdd+lptvvo5jR4/GkvDYKI2iOazMfD0cxpq/CiF8U1mW5EX+R8GHF6P8wcle02g0ufnGG5iZmWFqamo4uv3+tsXVEqCO1eW/YYiwcf2/0WwxvnkLdXlSjfJHJwBw7WHt+6T0+/PP7mM+9Plg/7M/icQqvJ4jFxA00DANprNN9EO5RJ4Z1OKX+T1N4JeVZIBC5PEvy5kyF/9Wp1006vVHoc+QRDdMHLWb6vMk7n6U+xIb64eSHMiAy+/TUA7jIhcghNQrYKKjcnksMfbTGC/nDCKBqS17ueee63nH//tdGs04gXe1wUhKIRbmZ+n347264vIH8+CrH8dDH/YkLr/8EUxMbcEHKMseZdVjZv44QtzFbRIzCapxbkACRUUi9uCcHcqRG6OImjTvIPUzBk+/V+OdsNgJSWQUtmzbxQUXXsoTnvQcFhdmuPWW6/j0pz/Cpz/1Ea6/4VoWmI+KxGPjmFSN+Ro/nqXKk3u97oezzL0a1ZeAPlLhsxuu7+DJ8/ZQIv60MIBlG75bmyfez5uJGZaX7idX/4EN/VBhXyvHF9h8TLk+O/BDX565dcuIHT1nxg+xA7BlWozZUbzWUfk38fM1LWRNC9IQZbnDUPrapBHaUZ9vQPxRo4hKEv4YlOTirqhJpy0QdfttlhFUcU7IbEa/rslcA5sZemUd04bUShyxhkEaEUk+ZYg3q8gNmctSr4ISBJrNNlmzxT+88X/R63XZtHnLCh0/Y6Ia8cyJI4QA4+PjPOGJT+XxT3w+Vz34cUxObqPynn5vgZm541RVhbMp1UlovnOWzJm0SOJMvDCYS2iELCvwPiAGisRATB3LBFXyLI9lYu/J8wxNk4T6/T4EodPp0+9X5HmDB1/zOB7x6CfzfT9whGu/+Ek+8pH38rGPfYhjx45FIGlq6pyNyP5KPaw1Ly/L6pH9st/J8/zffdCfQvmxjZ5fNBrcs/8ePvnJj/H9P/BCOvc3h3MD+3ZrnnE/coDe14xObyHLipOFaAb4wQ3+djxk9i2jtxxnU9XkQ+WXfqlXd5nOW+fUAVShYrI5QdM2qDXJb4lBJUSDT1UAk8Z4EZQwYPmlyR1BE/PPWrxG3gMmhfE26fANHEZS4cXEBiEdRhJpkpCN8wYrL3H09sAXG6FoREmpbq8kb+RJEDROCKqCoiGWBZ2NPnx6x8V88N2v5IZrP8bU9OTQ+AeGf/TIEQB2797Lk576HTzuid/G5Vdcgw8wN3uCuYWjlFWFtZIAPxf5EMSdvMjdsCoUkjQaKJmN47JD+j5FHiXJEBCXLcMBUhRjExU6MRGtFcbH29RVTdkvY8WFmsWFGUpnyIuCJz31W3j6s57PLTd9mQ+8/928773v5K59dwIwNT39tRwRPEJVn1pV/kNZzp+q8jeouhR0rr/GqxLv6+E8x5NVz5SIVwmyYt92S+m+rtEM36gCMDIxQV406CxuCAA+F9i07hs48zZZLIO99SA36ew1n+nc8JDMNs6p8QtCpRXjLjqAeb8YgTlfRuMbDMgIIUpmGUtNktxmObc/zupRjc07RoQ6of9xQriNScuAGWgTxpAEI+M04DgJMKiklunIB8iyqDfo1eDTvABxlkoVJ1Ee3BlL1nDDoSBFZhgb28bh+27lPW//I4oiQ8QNI7Hjx+KOf8Hei3n6c36AJzz1O9m67WL6/S5Hjh9BfU2ex4lATh0uS4pDqa4vCLkzZLlQVYHglSyL6kOZHQZC2FQNUYQsc1GbAHDWxFxUBuPRPVkeexrqqqbRyGPlwwcarUZEYYLStEJR5NTeszB/HMSwa8/F/NSLX8r3fO8P8973vIO3vOWN3H3PPqxzTE5OnoMR5F+BWIAxv15X1YfqKvuCMXJICd+jyt+fJGrA1zV1XUda+v2U7nV9VWCGikBDX7BhFBBz9LLXu7/Sw3duGEE0src2bz/GJb0x/pHrX7y/dx8T2fh5KLTCqGtTmJz5emE4z09UhmQnTZ1+QSF3BrVCP4AxjloDaiIwKLJU7jNpfqCaAV6QIgIhpRBREGSQU/kQwCa6sI20XV2mESg2Vh5qMWSNPDmNuMMOOgKtsRTOEHygMTLOm//v/8fszCybt2xBgYW5OXq9Pju27+Q53/ojPPmbvp/J6T1U/UXm5g/hvY/irc4M+xry3GGtpBxSh/Jj1lnK0oMIeZ6IoiZqC5L46M6ZpEocyJyNA2HUR9EK64YMSpfH6USqIYbwKVrIc4czcbBJFmLUYSw0sgzva6q6ptedp9OZo9ke5Yd/5Gd52jOfyzve+kbe+rY3cfTIEdojI7RarTORof9Krgg81Xv/oLqqbiga+bs18CRlYwcwMjLKxz72EZ75rGczNjZOt9c9tSxg/RTg/gc+qgaKosHI+ATen7TssFEf/xF18t7W/llMv25/uHftD6ABRwrDz1UFgAg4jdg2Gmqckaj0S9yFnViwNmr1ieBsFtt1k9Jv8Kl8x2C6b6JwooSYN8SR3ymPsG4gHW6Smk+c4ydmYPBR/58E9NV1SNr/Jk0FsgQRfPIbxmVgFLUxlfDJkWzbdQnXff49fOrD72Bycpyq7DMzM0ujyPi27/kJvvk7fortuy9ncWGOo0cO0ChS05IxZC4y/ZwzCbVLhm1ic1KWVIfqEMhzuzSPPmU2g1kEmbG4zEIImGVUY2tdchAk7YJEXRWNE4tlIKxq4h0Knsw6sHGorK89WCiKKJhaVzW1ryFUzM4dZ3rzVn72F36FZzzzm/nb172G9/zbv7C4sMDUpunokL920oJfrH393/KQv0Hhb1BGgIWNwcBA0WgOBpCc9uESLWYJAhhMrlzHINUH8qJJe3ziZHXH520U/quz/+DmS7KDM3zBHnjOlxdvazZc65wa/6CW7IyjbZsr3FsmBi+a1mUU+tA02WjACgQoModaQy9V72oSezDJa5u8gc0y1PeoUunNWInlQkCDLkl9J+6/JnBQgyJ5ZBEaZ8E5+j6WGF0W8+igmuS/03QfgbzZxGvJe97ye1gDCwvzVFXgEY9+Mt/1wv/B5Q99Mt3uIkeOHMAScFkEOF1SFbZDWm/6neiQ5msk/U0VY6DIYzt0XcdKwIDtiCrWRpKTGsHhhmG4GHDWxYhn0H0IqSIQeyYGI8VV4/ASIxLBQ+vwtUcEGnkGGvC5pdePVYzae3rdebqdeS674sH84cv/nGe/91284k/+D3ftu4tmu8VIe+RrIhowxnx3VdU/WbrqhMvtEfX6rYr+3brG6xzHjh/jxhuv47GPfQL9fu/0HcDKcF+SB1i/tBDzupq6qsjyYiOzff5GtX9t5u+x+w4zsRD4aNj/o0f7M4xno+ch+lcsQtO2ouxh0DhvT/1wcq8AnjDMjjQIxsadsfKJJyXRw8Y+ojSFN+14dQjx3zaN+8JFw7WxLOh1sMu6yPdPVQdNlQKS8UuWRUqvMUMOgfcBcXGyb+EsdV0xveMi/uOfXsHtN18LwPj4BN/9X36Zpz//v+El5/iJQ4iv4o5vo6Jw7qIwqAYd/ltEE/NhwPVf8vVFI4bmIc05yPPYBzHMOc2APRgVcE3CPKxhyDqyzqUtRRPvYUlz1kjA5hmKpA1EyTNL7WuKIvZ/xAqoxdd1jEh8iBOOQx01GrpzlFXG8771e3jYwx/NX/75K/i7v38D3cUOmzZv/lrABsY0yA+F4F8nmPcE9OqNnpgVBYfuO8x1113LM57xTZw4cfwMHMDy5OAUkESbGmFOcjxt/U+ylfTrDzW+eAdGZfvnu7d/C6HGYc4pADhwAAZLLhmIj4teDCEtfREhiAzze0HwEnfdWqMtRmHPZOQMpvwaao2j0NXUeIFgBJdneKKxq5hUEoyqQmKEKvUgSOozUIl4QwD6PuCBwkV8pRaN/AFjqBAswsT0ZhbmDvLON8amysc96Vl830/8Npv2PISZuaP4/jHyzEZSUSIK5c6khh1wmY0RQOI6IJDZJGGW2iONQOYMtY9gqDUu4gKDPoRU2omdi8kZpCqGsIQHKfEzBmXiEHXvcFawrkhOtcImrMBZwfpljsUYTNYi1H28r7EhfvfaSwIYHc4Kx44dYmxikl/79d/j0Y9+LC972W9y+MhhJiYncc59VVcKRPixEMLrNPAekGsULYD+mnUeApIJ7bOIftzy2af3RwWo65JNO3bTaLUoe+uGG1cDl65rlGLeafrlfHuxZp+de+6X5m8ld80Y/us5GQKzMpQSSzao52OxEsG0kCS3B5uUEEuDWcq3B2x9MZBZF5mCmUGc0KujcSngJSr3ioGq9kjuMMZQ+hoRhzhHkFQ3L4oYDThHcAPUPuCNodlwFLllplORZ1E3kNQ1GMP4wNjW3bzuN7+HxflZXvgzv8k3fe/PUdbCzMy9WJQiIfuKMNbOUA3UHvIsjip3NgJ8MZoxy6i7upQSmKiE7Gw0ekGG4b9NJbxBtcikwSWig0YqYjRA7FWwhsQMjDiAMYpzNsVlsayZuSidFhuiYhekTe3SQSvA02gU1LXHOYsxOXPziwRjaBYZVVXR6cwT6pLnPu87uOSSy/idl/06n/jEx2mPjNBstU5nTsVXWBogT6grv6Ny/l6bWU/g0cBH1rUrr7TbI+R5HglmxpzeZ8VdX9f0A2yUW0tStNng2LiLKbP/Yo7MMkrGTb17vv1oOUNhljeDnNMCQCx1pc7CuNYVTQM9hDSuy4AzsQEo+Bj6DsUykTQbYCAXFtF7Zw0uk9T7F4by36lHMobAxiR5QYu6LKYEqaDrNZJnTOZQE3GDCrB5BNeCCHmR4XKLWMPo9FY+9f63cPfNn+WX/s9bePYLf4Xj8x1mZw5FPUBryDIXI4DEPIzTf6MDMS5FHiaWHrPMkGcxFchzQ567eK2MoShiB6PLLNYJxkXDd0mu3Gbx+VlmcWkugXHxfZ2zSW49/TuzFLmlKDLyPCNKYiuZSzJmzqTv47DWkhUuahwYMOIxVnCZpdmMTUeVjw5htJWTZ/G1zWZBXfU5ePBe9l54Ma9+5V/z/d/3QywuLDA3O5u0Er9KD+U7fe1B9VOoXrahNkCrwXVfvpbDh+PMwDOLAAYqGXJ/u6osGyC57vHYjfL/0Mg/bmfmycr+1M3Vfc8NdZ/MtVcE/ydrWz3dqycYnMSOP1SHAzQ12aumXTBKLcURWyEBhM5YKokGLRJ7/WsN2Czt/qk1VzVhBNYlsY/UVyCx1BYEcAY/lBBPEUSa4mOcoa/KQtcz2i4wzpAPgEOJSkG172Ft4Kd+7x/Zdck1HLzvDppOabSzOAPQCEUWh4fkmSEE6FUh7v5WaDiDIQ4SdYZEvkmGbE3M95NzlITOk0JySbXmQcQwkKG2qYLg7FLt2S6bNWVlIC4zWC8JA0hEK4dNPlbxdcRhBtyKuo7grIgmElLsNfDBkGdCu92gX1UUtcU3cuat0On0WFyYY2RklJf+z19nx/bt/NEf/wFHDh1i89YtscLw1VcS/CFf+1dqcB8B/ZaNsuRWq8WXv/wlDh8+xLZt26mq6gwwgNWF/3V4AMF7itYIY9Obqat1RUAy4DEb5P/77Xz3xny+w1wevvn6uTuSnPUG1YZzkBJoAi2t0ThYkzQHRaPOXq1KHXyU3fYMw1cvOlThUTUQIo/fCHivkOjAg1A4mDTie1hNkTTlN5b4ag1RvGPABTBmKMCqYjHO0rCGUqFKtfpBhNLKLZYelz7i6TjnOHzgForcgY0DSJ3EikNIyi5BoZFbnIVMonPI8iZTk2O0RyHPgAr63T6+7KJ1CQKNZk67PUajJeQOen2Ym4PZ2QXqshfnF2SWrLDLZifGysHgRg2CoEFFIXoGTW3QSxGkhpA6GSNrcQBGDopPzcKlMWkeSfl+WVZxEKuNeItqAGeoKk+zjuBhZ7HDzIljTExN8aIX/wLTm7bwW7/9qxw5dJjNW7Z8NVYIHgPsVNUDCAvANHBsDa/Ge0ZHx8nz7IxwD7emF0hWoAErDSq1vW5wPI7Y/79e/v9vpqqYmCu51R95ym2LB2i6k1N/zz4aiL1+qkJmDFlWINZR+opao7nmqSJQSxic+FBuWzSi9UY0Dt5MO/tgOAhJrdcai1qoNQGHSQDUOsGn4aBDopUTvIlIuXEZXsADDRdD30B8TeEEZ4TCWVxmKHsLBAtZnmOsDAeE6ACUM0IjM2RGsKI0mm227Rij3YLD+w5x02c/xx1f/hg333Efs2JYrPrMLyxEAxQo8pyJsVFa1rGrITzsIVdyxcOfyCWXXk5rbIROD+67d55+pyTLLc1mFvGJoEOcINb9l6pFy3UAbVpTiiKaBqimfgznUhlCY0XCZXEhl2U/Dl6VyBLMskg0qusKY6D20SmNjrZwWZ92q2BhocPC/Bxzs3N827d9J5s2TfPff+nnOXL4MJs2b/7qAgajc39W8OF1NrN9Vd22ngPQEMjz/IyBQLeCHSgbE4EESRqAYYMGf56+8afY95vDR2mJ5ebOPc/u1YuMZ2OnWNM/EycgQwHQ3Br6Hoosw7iMqlcTqhoc+CD4sk6NLgOMI7UEp0GgZdDIoTZRX8Br1PazxlIFBaegJg7kcJFSHKcExsajMJAEk9hYpMYiziHO4onAV0iirHnKrSUh8kGEKpDy54g/ZDY+L4HxZEYYzQQnysjYOLsvbDJ7aIEP/+Mb+dAHP8Qtx45xVxhjAUvvxDHYupvcNBCfiEwKeIOvcvziPBw/RPuOkpE3/TsP35TzqJ1TfMf3vIBrnvgkAO472KfX7ZJ5S3u0iOlUGCgjydBBmrSWjCz9/3KPHoIk7kBs0oo6bXUqMTIsC3ofklZNxAEgVih8iBOSvfeReFUFqn6Frz39fsmxY0d5wuOfwp+94jX8/C+8iKNHjnz1OQH4Vl/711lnr1P0ivUsMysKDh+OpcDHPe6JlGX/TFIAlvXFbmBwsqSFt8HxyA0sOIQi/6ibXaDuzF+9rzy6hxAwcupo5Zk4gUCIi4RYBy+rPlqWqPdkRqhVSR26Q4qvT4YvwwEb0CocJRIJP06G479jqBsvjM0cIcsoFURCCu+T6GdqER5oDYq1eImRhTg7HPw5IA6JkUivtYPx3oZGFiMC5wx5EuVQYLLtaApkzTZb9rQ4ftdRXvfbv8f7vvBlri3HmFfD2LarsYsnaHVmsLN3U93xaeqFGULZW3FzbbNNY2yKfPuFOBPoTe3mA26Uf/3MrfzZl17JM7e/hh/+lmfyvBf+KFBw5HCP7kJJs+XIG1kk8sCwghA08Q0GEeNAQTkBstaZ6BzMsjVlDbUvEYXM5iAW70t8XaEh4heqIRKSTKQYS+pc7Pf60UHmkZzkvefQ4ft4+MMfxWte9Vf87Et+miNHjrBpy5avmuqAiDzX+9AMIRzCMJXG5K1g4GV5ztGD93LdddfyzGc++7S5AG7QJKCp9LWCFrg816gqNm3btVEJMAMese4nWLvPdHv35J0Oc7n/5n0LB1MDy+my+07dCRiEUgM11VClpyr7sTwnAyBK8KSWQFWCenKXUYZU5ZAo0mElTlCVxPIzybCDDHD/BASGOgqF2kg5VpPqBBpr14OJPiFFAiEBhtalmX9pkKjNlhiEzkVUXVK+3ciSRLkRppqW3Fim9kxSL9a88bd+jbf/x0e5vbWH5u6H0+iewNz8aWY/8LpTKLEovruA7y7QO3Q3fPE/4uIYnWTqYU+DHZfz1gre+fqP8JQ3v4P/8eM/xFO//QUocO89cwSvtEfy1AmYCEapBMsQi5E012SZLt2yyEABCWEJL0jOwhpJ1ysBkGKofZ3Wg8GYgHpNTUcWP+eRFM1ZKywuzvOoRz2GV/zxK/nJF/0ERw9/FWECQk7gCSHov1tr9quqXe0ASOloUTTOKLoxOhwUmtzAcC7d6kdsaV0+UHjZcSGwbd2lZe1nzPwCIycWOMziQ/ct3EfhGmeW1euplwu9enqhF0UsBhN8kVgKZMCGEwZDUwwS1XhNHMSFxvbaXq+OO5JziMYwdzn6bQx4haoeDHHUNClYh5Ldg+k+smw4SB0J9DEVMLEsJqms18hjP0GemyQiGn/WQLOwbGpZsmaLPQ+Z5PoP/jM/833P5A8+fQdHH/pcpiZG6b7j5Rz5h5cze+1Hz6q+Ws+f4PiH387xv/9dGp98KyObtvL+ycfyzJe9if/y7c9j4fABdu4egwDzJ7pJIt0OatlkLoJ9NjUaSWqXZkgaShUIjfThoB5S+zO+QkKFJsalERPfL3VvDuRaZKBanKKOzFmyLDIhm40G1gr77t7HQx76CP7w915OnmUcOXpkhUDpVzoYmKoY8wj9tVL/8f5mWXZGDEgjK3IzWeEMVj4Y9nqvczxq4/p/dq09fITMe+7qHXnUTH+e3Jzdxb+/8zRi8Orp1F0skQ8QF1odw+eQQKgoxxnr/USefor+o2AoirMGDTEXjfMAY/XApJ2NoEjKpcW6SJ3VwZRhM5wQHBKVbvA7sQaT5gCqkTjey1pcyqXzbDAuzJBl8fmjzYzxwjCydRPb9rR4zYt+nB/5X7/LrQ96LtNbtjLz+l/j8DteST1/4pyvwu7+Wzn65t+n+Ogbmb7myby+exEPe+Iz+NDb38T05iaj421OHO1E3KVwkQxkBJe4BEt8sxWzqpdJqio+DatzSdU4ag0k4nKIbcRVFYHLLHPkaSq1MfHfqEZh1tRxGMe6WfI849ixozz2cU/iN37tfyMKs3Ozp02a+U8JAow8I9RhSZ151dTfTqfDhRdfxFOe8jRmZmZO3wGsLfnpMlDgJIPGVx5XnSSM+TTBI1Z27Fs4eKnWFUbO3vueLBoQBFXPou+SkQQsg8cCFkFSVOBS+cqiGI06gUtzEQZOYPAchkYfCUbJkww08C1DvTxNk4SwSjCGokhklkACCeP7VBF/SzgDjBSWIlsi4zRzSyOPg0Mm2xljmTC+e5qcOX7+OU/iTz99mC3PfiF8+O859JaX43ud874gF/fdxOG/+iW2yCyHnv4invaSP+CPfunFNJrC5KYRZo4uoj4M1Y1OXsmRJSCS6PjimPNYLg0SgT9JYOlApzFqOw60GyJl2ViSNkHUeMicoyhie7FJnI5jx4/w7Oc8l5978c9T9vr0er2vhjkFTxIjo3VZU3UrylWP2WNzfPNznstVVz+ExcWFM3AAy+v+Q6E8WWHzS7Py6o0u2MM2sNIKMZ83jQY9qZ55sHd8mSb5OSJM6foOAIWO7wygN6y4YbZpRDCDWobqUC3IGIc1drhgB8o2A9KQtTnW2GUdcgnAMpraZ6MwRqTdKiEIxtjI9xdoNDJG2wXOWQZ0DVHoB6WVW1zqGrTO4IwZUmNHmxljmaG9ewrm7+NHnv5YPtR6MLsf+xQO/emLmL/lCw/4qjz8b69H/+3P2fI9L+a/v/1L/PwPfy9ZBpObR5k53k2R4kbS5MumBZE4AyGqNhmF4Kt039LERrOUcg26Vaq6xoeoSzgACPPMpWg1vn9mLY1GnkasRy5Lr9fhhS/8rzz/ec9nYX7+VIRt/7MPN4iwNayNAKx13HjDDRw6eJA8z8+AB7D67qxzLYJ6sqJBe3R8PR0AB1yzvnuxd9Prn8gW51m09TVHuidSyefcHmsBwqj7f7yeo9frkM93o7pv6lePmn5R/XfAzDMW1Bq8EbKUiwdn8Om5vpERJrIIGM5HhdxgDbVzkeKbWbwxVMbhxpvkDcN8r2J+voc0c+osI5QQuoGOMbQajnbKa1sGxguLM+A1fr6zQuEMjcywuWlpbx0jzB/ih5/5NO56yLezI/fc8+pf/E9dmYt33UD/VS9m+8+8gld86F+of/C7eeWb3srYRJvjRxaY2txeAv1YKVU+1KiRyA/QZWShOLEuOgWX9Ap8qFOXehJkJeoV1KGO6UCjQQgLMRfGQ1VTVXUUPyly+v2SLHOoeorM8Ksv/RX27dvHtV++dthF+BV8PAL4wHq2mTcKPve5z3LfvfeyY8cOFhYWTte7LDUDLfEAVn5SCEpeNGiNrOsAdrCR/Je1t0tnIRTzs9yn/SuO9ebIzPnhZy8nDimQm4zD3aP0pEb2bKWUkHTRIhgVJJadsoajUqFSUAvBGoIxqLV4G7v2vDVQV5jFfhzGuX0S2yywGgd8DByHJlBvdqHP3FyPqS1j7BhtULQL/P+fvDePs/Sq6/zf55xnuffWrb27ek93OnsgCRCSEBIggAiICKiIogLjMuowOi4zoriOOm4gDggoig4oCAouICBLANkREkhIQiD71ntXd3VV3Xuf5Sy/P77neaq60x26O50Q+D15XbqpruXWvc855/v9fD+LMQxswHQSamOYGktZrjz7BpbcaJwTZuF4npAayFNFlmrWdBPyqQk6jHjZc76TOy54Hhv8Ejvf+cZHxJ1pq5Ld//enWf8zr+ENn7qa6Zf/OL/7hr+m081ZPDhicqbHasbn4ZsALXuyoZ9JR6Bp6JlCHgqxvA+42uKbhAfvcV5ARDsYCE5jDBlASPHO4WoHQRyQQmwFh6OSdRtP43d++3d52Y+9hPn9+5lds+aRzBE4+5j3vff0+ytioBMvL1YFBaojLINXzSPbwIckTY/k7207jE+w+skl6W3m4F6Mr9SiH511qFgi0Q+tQKOpBlKVsLSwl4XNHSa/9xkUxZKMk5K4uHWU5CaJzP+1wmlDMLoN4HQNzbWTkhxYJn3nf0h/+bzLSfOUzHuMhixWDXmimRhLue4TX2d+x0GecPnpPOpRG7Cjim6q6eaasVTTz4UpePfBkt/78L1YGwhJwCgjQqFI+JntGdJuh3Xr4Ref80JuWHcJG0zJrne/8RF1dwZgz5//InM/83/5vQ+8g3Nf90f88M+9gkMHLMPlkl4/P8YmsGJT3ciHVaPUbDzumltSC/nJO9dmUjTtqnWOEJVwWZayvFyTpBlZJ1DWA6y1ZGlGUYwkX0H32XOw4sLHPJ5f+Lmf57d+939TFAV5nj9SK4FLH6pvrA8v+4XSqVYl6XxD+A/OeIDvv1MFj1d++mC1vLm0ZYy74iHfBDSGwpbsNQPSsT4hNYQswacG3/w9S7BG41P5u/xp5M/UENKEkCb4xOA6KVk3JclT6OaERIuph9GoaPWlEo3JU3q9jLE8oTeWR6cfGU2liYzGGjfeDRMZeSz3E6PJE03XyLx8LDUYlbB1a8bf/fqv84H7KjZuO51d737tI/KICsDBt/460899GT/6qrdy6zWfZnKmy2hQ49zRTya12oQ2xNpzFQbTiIlUlFC7umz9a+WfGyWrZCaKU3GK0UrsxwN0OzkTE32UVvTGeqRZRp54dBiycHA/L3jBC3ne9zyf5aUlHsFNwHnA+EPxjZP23Yt89SY17ASujQ/wb7diNCHRZ+wdHuxY58hN9rC8YloplhLYv/MuurfsIi3mcUhvH5TGG40zmiwxcvorOfm90VLSK9H8o4BOSrI4xA0rnPWoW3fgx3IqLwaZlsBQKfLMUHVTFvYvcfvdB1h7xz7S3FAXNWlqyFJNJzd0M7HUvm9BWG/dVJEZRW7EGKSTGNZ0DXNbx7jho5/gD//fPzD3/b/Anr/4xUc0XF0Pl7Ef/AvClT/ED/3Uz3HNtV9iYmaMpYURU7O9I0DB1R6UQezP2iCWphvV0b1Z8ACBBSPtGE0IjixN8AHqWshBzluSNCFxFnFLV3TyjOGoECai1oSywLiayjpc2uXHXvqTXHPtNezYcd8jlS6cRpztfp4A3nuSNKXfP0ktgDpy4hfuTwZsyEDHuDYd6xxWynzZmwSn3MWHikWctejsgRHXBzUjWPWlBg09xcI9d9J9+3/Qt0vCUY9Fjzj3GHSqsDq0pJyKgE+k7/dG4RODVdLjm7XjWK0o3/tFijzFJRDylMIHSBOc1lil0N2czZunueWGnXz5+l2E3ECW4tKUyhhUZkiSlDQ1rB3P6BkRAKVa0pHW9Ax5t0tHwR/91q+hrvpRiqvfijtBnvc341q69XrmznsC16Zn8YbffSUv/43fJwSoK0eamfuXDe3pE6PGvY9AsYu8jLAaM5QpTmRaGi0JRbYW2bB3Dmc9SZKQpxm1cngfWFoaiL7DOwprKUcV4HEhsLB4gHXr1/NTP/nT/OZv/zqj0YhOp/NIbAWOugHkec7BAwf4yvXXc+WTnyyVzwm1AEcCf00Q5f3+O+Z1dA6AMgPK0T16sAB5vnm5HoJ3x/w+CkXhKxbsEot2mcV65bFULbNULcmjXPUollgqFlkqFlkuFlkerjwGwyVYDtw2uIc6CTIKiow6MaSUPttFP3xllPgBxKAPbcS4UsXYL5MawqAiFDV6PEflorf3tSPRmkTLAs4SQxIC1aAgSw3TEzn9PGEsT5jsJqzvZ8x1U9b1DOt6hq5RdIwSb3yt6GUyGdhyWsq/vvY1fL6cYY0fsPjVL/Ctcu1/31/RP+dCfvMdV7O0626mZnssLRZHrvz77d3NRCpELsXqf22Ge0rKO5FYGI330W+AxpNQhEV5J6fb65AkjXlIh4nJ8RjaAtb5GHEfOLR4iKue8nSe9czvYrC8/Eh9Wbcc7YN5nrO0uMhXrr+OsbGxk2kBFPfPCwj369Wcs0LRPHzelhwTodRmH+WwNOUChTGnLVUFqwbsh30fhcIGi0YxnU6s7L7qKI2mOv5ywOQVu9UiB7OCPO8SXCUS3WBQPmCNgkROeheZeUoFvE5wBkKi8dqId3KisTEWTfsAiULrBN3J8EZIPVknxZsEF3n7VslgWykDaRK57aImTI1YZ3UzRa6hpxXdRDHbNXR7Pcr9I9723n8j2/oE5t/7Br6VLu89yVf/gwPrLuL3/8/v8Qev/yuMkryBbHUVcKQHXaANam3GOY14qLlNxYxUdAbWikQ4SRO8Ew5GiHoCbRQ2mo2YJMHWNXUMMe10M5YXa0KALE8ZjgqquuJHf/gl3HDjV9i1eyfT0zOPtCpgy7Fea+DkWwBWubesHqOtvpy3TEzNkub5kf7rfWD6GHfBDtIc0+uxPFg4b1SNYkIu99sEylARfOAPzng5j509n3k9EJAt1YRU44yC1Eh/nsjDm4jYx4/5BtU3pv17MJpSeZbSDoMkxWuNi+673oggJyQyEfBxjOeiyUdQ4LRpE3GNkudgKkvywS+iakv3uZeRTnQx1oodllHkRsZ3U5NdbrvuHr7+1V2MrZkgSYT/H7wnNwmZEYJRR0GmIVVBLMoIbNtk+Mc/fRM3+RlmhnvZP1ziW+1auOZqet//y7zl2uv4tb33MbFmM4cOjshmuxzdCKaRVh3mUS1tQTMhMLGBCxJLrkJogUJtFKky1FaqgLq2eC8mowpIjKEoKuq6piykTFZa4WuP0ZqiHHHGGWfyoh/4QV7zp6+O6cePKL3AaQ8JCKiONvY7kgfgPL3+NGmWUw4Hq0/vGeDoqJ5J9oXFPQRXUga/prQ1pgUZD98EbHCMmQ4XjJ3F5s46xswQnxlINT4zuFRBYvCpJiQJPtH4xBASAfJCamThJzK+k7/LuM8nmgNhxEjZONtXhEQ+32nwRloDrxVWG9k4tDj7WvEDRyPGHeKBp9HdXDQA6yZRicK4jDTRZFqRm0BmNL3pMcanx9DekxCgtuR5EmWwsuDzzMgIUUE/00z3Ejp5FzcKvOejH8dPn8biZ9/Jt+qV7fgqu90kb/9/b+anXvHbeOta74CjzhGaQ6jRCKhICoriLCn/NcGJFkPnORCoykIs14JYn4VVd3GWiatQUVYrmYadHJMYBoMCZ1d0HUU54tnf+Ww+cvWHuSEShB5BgOBWhJF+SmWMCUdQNMNR7MCUVsdqAdYeqyhXJjnoR/OEetAvg+vX3h5mHd0WfiFggqJwFbcV9zJlJznglvHBgNcEr3FOUnW8NYTExPm9LO62AjAG3yzeWBl4o7EG0rSLpaZSNTox7eckqcZrK1WB1iRKqo0QyUCmUQY0phZZgl62ZLWlch5/YECY6uKtxdYNviAn1Wi5YHhoKC49kaBilHAFPIEUOf0TBR0jBqW9VDE9k3PtRz7G9UuGyfGKQwv7v2U3gKUvfRTz9B/nbZ+4jp96RaA31mG4XDE2kd9/bqtWt3grLMGoxiCogHcBvJM4NxTKOXzweB9dibVBay8mIjEFKQSPjYlPeZ5S1jXeSkDJ9FSfoqxYWFgkNYbhcMjE5BQv+v4XcdNNN1CWJWmaPlJezllgDFg89WPAFUjwRDH4mWM3guGgSlJcoqfKspjw0bzhaC19R2fUwfHK21/P1D3j2HaTi0WgOkyscMSUYgXAOMzKLBJJSl+zpbeOXz7rpVgvyG9jfmJiv+7bB+LxZ0xsCWRS4LUwBMkMofb44Mm7OeUHrsFOdKi1oqg9KhVegIR+aCrr6Y91xFNAiQGJrxxJbkg1JFoWfq4hUwpXezZOwt995iMcqA0TO27hW/ly5YgJP+SahcDXvvgpzr3kyczvLe6/AbBq+hRWHQ8quowC+LqxyRLpAFA3BCClMUragtDYiHUyfF1T1S6OBR0mMYSyRBtDRytJQVZQdHIGgwG18wyWl7jiiifxxMuv4NOf+TSza9Y8UrCADrAZ+OopbgE46ql/nNcD+XotoBVeh7HK2+xIcciRcGOmUw7ZZfaX86zE764aFYUj8clwWBupwlHwyzhF2ufv5Z7+5Tx6/Ez2lYfQaLQyBBdQRvpDtGrxgWCEF0DEBQR3MFgUPksI/ZxEQb08oloeQp6iIq5g1UqVkozlJCpFBY9yCmUFBOumAmKlSpB/7QO5gYluTrHsufbunaj+NIMvfYBv9Svsup3CTHD1R67m3EueLKnCbRtw5IjvqCfJyhsJLQDonUNH8ZSkAcXplXJikOosKIM24gWhVdV+jbWSCbU0GOGtI0sNodelqirqumKsP8F3Pfu5fP4Ln6csy5MS2TxE11mnfANoAdjGHbxRAB7WAhBNLu73PvWP/c4zCkrhdJisgzsqqL96E3DBM27GwIytjB5OkUjrwGieL9m7eMzU43CjQgw4CoceFmidtuO/oHw0CIoiocZSHLDeARrtHAwKqkGJSg06NWA9ppsROglqaOWU0qBqhypqgnXoNMHbIAGhuXj/dRJxwcmNoZcoJsbGuP2Gm7hh7xL9XoelcvgtvwEUd94AFz6HG3aIR0GaptTWHz4NONr6jxFfSkmWYLu/R/mwZiWnIgSRWEtykcF7R11blJLk4tbw1cQNwXpGZYmK1mG1tVLqB09ZViwvLXDJJY/nksdfyuc+99lHUhWw7ZSDgIEV91b0agAwHD6tWR1IsHL1jr0BhAoV8JpJh3ugLX7VXh8OBwhP0Q6gtOHWpbs5UB8iMYYwLKjXzbJ85RVS4ocQWYBGrLwkLkesvaJ1lY5/Gi02XgRJtMkyg3Ke+vq7cAeXmb30TCZOm8U4SRvKYhBHJzOMj6fsuucQ99x1kG5mUNaT5gYdPJlRjHXh1q9ex4JTcHAP3w5XvXiAtD/G527fBbYg73So22DZVfqAY90hQdKE26lRk+iktLAydQQMvaBjIUQHISOjP2tFeF1aGQMao8jzhNoKlgDi6ETwaJPQ6/XwrmZyYpynX/V0Pvf5z1HbmsQ8IkJGZk79FGC1WUtYXUsf66wOR/YlR190hEOegFNuvEnIPa6S8cgpwaoR5cle3aTLLYt3safcz4beekbFInbDLKMLz4BhGT3nGjGQ0IElrlu3Dkk6vliJEsJQoomBnoqsl+Lu2EM4uMTEGesY3zSNqSzdVEdqryZPFWtnUoIN3HvHPMqD9oEkgHIBfCDVcOctX2NQWTpL+/l2ubJ6wB4S9t5zJ3Pbz6MeOPJOwtH4JitMVKm+giiB2k1AxQCC4Bwm1agA1gro52MroGOeuVIe6yqKosRa8RvwPno2p4a6lMpUq9AmPiWJYWnZcuDgAldecQUXPvoCvnLjDczOzj4SqoBTPpLQq199Ff0A2wjcwx5H/foHyiIaBBWwhE6IjrDH+wLeb5t5kC98R+csFYtce/BmJjsTuASoKnQtfn8qBFTkm+Pl70oyQBp9arT9irZhROZZBBSDDfiyphpUuMqK718MxgzxXfMobICqcmA9ygsLTYdAnmiCC2gPew8cxNeeMDz0bbMBMFpiVDluu/12OY2L6phY0EploA4HctSqgyD4lTbBe9I0Ic3H2jwCo8URqrG5S4xEpaGaiZYjCgXasFsXPMF7ilGBd56yqhjrj/PEy58Yx4ueb8crWTl1oyzzG6QDh6OAiMeoAQZy8/tO0DKmOWEAiftzBk66DTAJX5q/ie/aehVMjmPu28Pk319NMJIWFGJEt4sGnuLhj/gGKI0yifwODTZgDEGDyxJGIeAXBnSmeix85ussXne3JOck4umnjSbrpHS6Gd55urkhIZAo2VgSDTqILVlpcnkeiwe+bW6ysDjPcj7Ojh0741mjjtE1HgMRaMY+q/QAAQliIThJb1K2nRHV1mNdwDkrdnBxU1FK4ayl1+tQl4ayWMIhPpfOB7yz2OCiSxB477jsssv4p3/5ZxaXFk+KavvI3wAUHM0VaHUnoL7BIj3a/wsh1IGAV16bRMQvPvgHvwmcREsQCIylPW5bvJtbFu/mvJlzODQ4QHrrPRhlcM5jUgn2qL1HGYn6ckqsunVi8NqIH3McDTqtUZlBpeInYCZ7kCUU9x2g20sZEaO+xnKSPGFJaZROmFw7Rj6WkRlRAHrrwTmUyiiWAmXQkHdwxeDb5ibz5ZCQjLVuNdo8UDu4ktAcVr/PwROcvDdKp+ggISJog7UW5Wp5z5xvTURUHA37yAtAa5wHZR2VrUlSjbMBFzxaKayP+gAl9KOyKjjn7HO5+OKL+eAH/51+v/9Idw46CQzgiOWtjrKwwwPVAMd8G6mDBhth9CQxJ11GhSM3opOoBjKdsjxc5Av7rudx6y5k3ijU7DhOyViqBlRDMgq6Pfkl5itShGMICFrEKV5rko7oz10IKOvoTnXJ8hTjA6mS79mf7omRpVYo7wm1g8xQjyrSPMHZQPBQlRVFVUOawf2dl751L2fBhza4Umt1Qtt/aGzajbxXti5E3OWFkxFW3RLBewkMiRoCoXVIzLpCmIDFqMBamdZYW8cQE6lMOpFdOCpqlpeH5HmXJ172BD7+8Y9T1zXJt3Li8FExgFVjP7XyP/eDAMSN9diPo1ylDwESo3r9PmmSslK88aA2gpPBBQKBLO9yzd4buHuwk06W44NDxdEfRCKJkz5fhZUe01orp0+0DndewJNEixrQWx/55hodFMVyiYsqwdQoRosjbFmjYpZeaiDYaEsenZjke4h/PjF9+NvmihjSNwKC1ZHHzGoMJn79cDRkeTCU98GK/FcMWMUy3CP24R5WpTX7GEQaI+BjlkAg5i1qZIPQmm63Q5ZlaKUYHx9jVIw4+5xzOO20LQyGg2+2gag69RtAW24d7Wep+wMyx1pg99sUfCYsvuCCgm632wKBzX+nZBM4gY2gl3bZvbib6/Z/lenutCQBhxBz+xq1mT6MZKjihuhDQAqYmAoc2xm1ajOSm9KKTTXgrcPVoQ0UUSEQnMPb0AKPKsjid9ahdSrCiqpEZ51vn/WfdSAuLmDF4/4b3NnNNEBrHU8hw+KhZYaDobwvWsBWkxhCQAJFndDV69pRV5WIuqxsCpV10YC0sSELdDs5aZKJO7QKDAZDRqNSrNkTg7OW9Rs28qhHPRpXfxtVZSsbgDrC8uv+dmC64fDHBJfjenifKgXO26KuK8b74+LGE8Jhp/LJbgInUw00WXWfuO9zLNohWhssHq8kDaQOAes9bVxQ+7NWAtScc2KMGsBVETAiEOIoT5JtJKHGNMKV4CGeVMEFfGUJ1mMrS13U4DzeOon/CiXUJaY/9e1zk41NkmcZa9eId6w7Wit4JAFtVYyYj8afAHv27WXh0EEhbkW3IB/BPq11jBoPrYd2VZb4mEQckEpBOhAnydDO0qgQszxjfHwMpRVlVXFocTmSjeDixz2OTqdDVVXfbhvAEduvEu7F/R4r1djqR3XE/18dJ5YGrdBZOqy9OLYYk9xv0Z/SauA42oCJ7iS37L+VL+65ntmxGZSC2ju8kvAJ562AQUHmyi0wpbRkBKz6WcIWlngxQ2NdhfDSrSNYjwkhthUOnG+jsLx1ggV4qQCCFRfcqUxyBPW30QZAf4bx/hhnbD9dgKcsOc5dPlZHzqJ0yp69e7jrzjvZtGETdV1ja0sIgbp22EYLH5mbPjh8IDoEic+gCpBlKWmWkudZZBFKoEjjOjwclVjrCAHyLEUpxWg44ozTt7PltNMYjUbfXhuAWh3foh7A/vPoQUHlMR1DQ+jEr1vywTM21qfX7eKPYRD5cLUEJu55/3HPp3D4GN3VhIAIUtxWO61/iZMWIIQYUhFaUQpB5NJSoMbn4CMY5f2q0j8CTTHgsqG6JlqBE227rRxz0+vpZAmqP/3tgwFmPXpuyGmnnwEB8jxp28Yj38imhfRe+nYXpHJSSnPvvfeKn0Kng1Jyorsmi9F5yrqmrCQPQGtD8B6lxRiktjVJaiQfwnm8C2ijKcqSqrY476iqiqKqUAp63UywGe+o64o1a9ZyxunbsXX9bVgBrGoDlFJHdwQ+Ovg3av/e/LeCAfSlIlAH0dDp5uR5/oCTgIejJZAqYIqv7L6RL+6+jqnODDZ4bPCUVjYEbbToA0IMRdWNB71ITYMPJKIfwlsZGzWbQJ4lDfOnbQdkrEQc+cnp761Dxb/j5eODpWXOOP08elphZjd8e/T/JqHymos2zZBNrqUcOlawQNUKeYTOG/DBY5281s2m21Ri+/bsafGYRvKrVBDzj1ieeu9E7OMDzkklVkVVoLWOqqgoy4phMcLaml6vKzmEzsX0JwkgzdIUY4y4Co+P0e12uODRF0igzLcRKUiz+uBXh6O2R08iPewxFP+29o5f9WAKFVBGDZSSGK1O3vmGXICHoyVIojPRR+74mNB6k0xWs5L+1EUJso/9quACzTYpv7vRCqMEPQ4uogRxIWut8C6CTSFAXPjBOrzzeOvpdFO0hmKppC5qbGUZDgecdfaFbOskVL2Jb4sbrLv1PPyBfVy8RWjsZVWJlqJ9n6LkO8QNNi542RhUDPXIKIqC66+/njVr1mJSyfyzzsmGHFbuz2azqKsK66VyI+I0SkGaSXxYkiQoNKPBUJKGlMS5NXhXVcl40HpHbWvKquT0bduYWzt3wsabp/A65SQErcKRNf7qgMxVlqDhaEh/WDqy+m4ePviOl4Z4pI0Z5Z1cWoDj3D1PSTVwjJYgEJjsTnPjrpv4zL2fZ3ZsNt4E4Agy01fg8VGMugq4jFbTVeUoK/ExbH9n6ynLmrpeET+5SiqG4AO4Bv33FIsFdWGjxRVURc1gOGDDpu08bnKMYukQ3dPO/ZbfAJLNZ6NTuOyi82Vh1lGXH6m3Iu2VMl/K/RgV7j3eywI3JuWTn/okO3bs5Lzzz5PWwDqck/7fORfThC3OSetg3UqQXghBcgNVoK4kIqybZwTvccGTGEOeZa2nXq+X0+3l1LXFWsfS8pDhqGBu3Rzbtm2lKIpvrwpg9d6yMuYMRyyno5KAFo52esuCUZkLFvAL3bHeoklTxnq9mNwbHtJN4HiqAaMlzOP9t3yApWqZLOkIJqACQck0QMV466A03iNgXvx2LubWo+KI0PmoAUA4AEbqhSST085ZJxhDCATrcGUt3AItY7FqWBNqz2BUcNG2s+jUI/KzHvctf4NV01vY1nE8/bufh7NCAhKTWTmtXXDYEFsrt7IpBN/Ye8kNee2117Jh/Tp0IjmBla1xLroDxcWvlCYxRlqAiB0QVipK+bi0DKOixAfZjBJjIDiyLG3nYM6Jgah3DtETGKampti4ceORvpjfBhtAhEhXElmOnAOycqMHv3qhH2z+5iOnunm4UE8ZJkh0b6muioELgampaZHjnsDCfqg4AyEEpjpT3Dt/Lx+546Os7c/KT1LS9zchKc6vlDXOx/LUB6Gge/mYRhx/Gn260oqqsLHf920Csa3cCnEoYi2ucpIqHBWvhxbneeIVz+E0N6SaXv8tfXONbb+A4sBBvvvsNaT9GRYPjOj2hBIu94ic1iHSd5uxnounvw+BvNNh9949fOmaazn33HMxxlBXq05679q2MkRUNs3SmDMYUFqjlWJUCEdAaxMrC9cyC4uibPVGhMDy8pDlwQgTw2OVCpRViVKKbVu3oo05KQfeRywI2ESBEY4O++sgwEdDu1wFzuzxIdjmDT3sP19PJWoSo7ooFXZrBROTE3Q6HZy1J7GYT8EmcJRqoNsb431ffx+3H7yT6d4spbPCN0+EG94sek+ECXyUnwp9DEIQb8EoS3WRFeh9wNYSWtH+WB9wlYs3r6D+K6SYQLFcMBgO2H72BXzH+rUMlxeZfOxTv2VvruyCq0gGe/ixH/4BqYJcTaeXSKkeZONrkn+Cl/Rm5wWhd5E6nBrDP/7jP7Ljvvt41KMfTUAJ6OekN/dOqgdrxcxjNCoin1/hncNa2+ICQtSqMcaQJgatJFxEN5Lv6CBsIgfBe0+WS7RYlidY6zh92zbGx8ex9ptCCjr1TEC1mgccnX/uh//phHK4iK2LWCW0YN9B8OX9AAB5wadDsLhQk6bZTcYYxsbG5MWr7UlJfE+2GjjWlCAQ6KVdyrLgHTe8E6MTOmne0kqFgcZKNRBcBPakYtJGIqqLYSV6c6XiiEncb0PcNOrSRrAq8geC3JxCcGk2Aks1rLGlZf/iAb7vOT/E7PzdqEdd8a0J/q07jYM+42nrNBc95VksL5SYzAjIF/n6IchIznmP804QfCegq7WupQ5/4P3v5/GPv5hur4dzMtJzDcrvBG+x1grBSCmR9jYjSC9YgFKiSDVGrF6lEpCxrta6VQRqY1AxZ1DFcW1V1YSoLpyZmWFubo7imwAEKpRvjFLvH9mjTrICUEf74sO3AKVUnHuvCC/iowiBXeEo8wFPmPUEbO3Qih0S1NhlvD++ggGEkz3VT92UwAfPVH+aG+/7Ch+786Os7a8XRaBWsb8UdF81SHH8TnUpJ4+Or5/R8lCAtVGJ5gNV5VBBoSJf3fsIetlAqCS+2te+NWasRjUHD+7jwkuewXdN91lYmGfmku/81iv/n/pDcPcN/O+f+3EAlpYKxqbyNsMv+Kal8pGhF6idk8VfW6qyZHpykn/+l3/lK9ddx1VXPZUk61CWpaQBB5nE+BCorW3bioYQ5L18jlrlHdn07hGkluQnrUnSJI73XEzBls2pcRuyVp5zbWtmZ2fYtGED7pvAB7DOlra2WHv4Q35ff1KxevqoVYYi0mFXHivalMOWuodw0+Hwnxf1FmwIweNDjVb6LhN337Vr18bF9c3bBI6cEmg0eZ7zzze+i7sP3c1Md5baWUGItWimpQIIbZ8pE4FAXcu82llJqlFKYquCX8EEQhSohBBwEbUO8ZQj8gS8FRBstFRSl5aF5QX+28v+J2tu/Txc/jySvPcts/gnHvUE9i/VvGCT4gnP+l4OLRRkHS04ScOniBuhC4HaeXm9nROthLNtdsAbXv9nzK2d47Rt21BaU1U1xMVoI1fAxilAYx5SDEcrVWZoqgQ5zCTi3ogoS0vYqDGm5f43t0WWpfT7PbwPmCRtJ+O9bo+5ubUteezhOfrjaLKsRsWooCzKwx4CcNpj5C0c5wbQjv5VuH8VEJ+AmOaEwwgaPoSv+1UlQcuS83a9Is1SPYnS4RqtFEmSMD09TbfTPRxEeZg3gSOrgRA8/bzPoBjw9i+/hdSkdJMuAQQHaOYaYcWUcuU5Rw1AkH5fVIOrOOw+YJ1MDGwtrYEOK2NVARXBVvIGJpnBlpb5g7s541GX8/LLruDA5z/A3It+6Vti8Rtj0Fe+iM5XP8Kf/ckfyum/MGR8KqcqBblXDXLvI+nHyqnfSHnrumL9unX8+4c+xMc+9jGe//znMzk1Hcd7sa/3YeUE9NKWOSeveWtvE/X/TdWmlSJNDEpDkkq2Y5ZoeQ9iJqYYvsRqLUibp/FM9jMmxzsYo5ienkZp87B7AyTGlGmakKTmsEcz1TqZ57OKCLQqI1Ad/RE5rhxB+tkN8sb54Np/97gJFZJZzTha+3u1Nss+wOTkFJMTk602nG8ws3+4pgTee6bHprl5x038y03/xLrx9TKy1AqMWuUsI+alvkmhCZGZ5gVIdVHZ510U/ji/iuaqoklFwNmVcrQaSWpNXcgpOFwssaXj3j138VMv/wOuqvews6qYfeJ3P+I3gLU/9CssfP5q/uTHnsum8x7Dnp2HmJjOhU7tY98fkX7rIyvSW1n41lLXtSxE4Ld/87eY6Pd53OMex1i/h7O1BH3UdRScBUKcBjRnlncuCrZcbDd8y8gMBNJOjk57qHQMk2aCASgw2rRldPCeqq5ZHhRoYGp8jMm5DfQnp+jkCRs3biJLs6OLmh6Kq7FET0xpEoMxyWEP4mZ3MstAH+4GpB7QaKNF+g8nA93VegIc/nETgnu0DwUmMYtGm7tsXTPW6zI9PbVqbPPAKP3D2RIoFGPdMd530z/xhXs/z1xvQySZxGancZlZEf7hXSBNtCgNVaMapO1DCXLy28qitYBe1vloO9Ww4CJpqHbUhUVpGC1VlMWQxbLgtb/5ZtZ8+l2Ul34X/e0XPGIX/9wzf4Td+xb5vo0l/+3X/w/LiyUhePIxCQZ1vln80hIRWyEXCT/WCZK/edNG/uFd7+YLX/hPnvOc57Bx82YgUBYFPpa6ohWo46YSsLYSVWDwrTmo906MW+L4WmmQrivBe0MwHVSSQAjttCCEQKeTE2t8nPdU1lKXJSEEqtoyMz1Fr9c9qWnWyXUAgsGNBoP5wWDAcHj4A6Cuq2/gtPSAIOBhrcYD9iFHYQPe2ahijqQO+WAv0GEcTU6eJ19HQZZlrF+/AWMeoIT6JlUDnkAn7ZBozV9/4Q3sWd7NdGeWsq7FXEIRk4Uafzo5WarSt+2RczHCKr5xPrrTtG9kxAtAUVeWcli3XycONrKT29pRLFfML+5m7enn85b/+fss/8OryX/gf9LbdOYjbvGvedLz2LvmfM6544O88x/eiXNwcH6RqTUd6rJugTcXS3jhPviWzls7mYT0+31GRcmvvuKXydKUK664gpmZGcFakJRqFVR7ygvmEo1UomeDUkQ5djzVNZgkaToFcRKKjxBWpjm0IHfAxPfbek9RWYaHDrC4sMCwqOl0u0xMPHyjQK01Pjiqur7Pxipp9QOgKAqMPvEw02S16V5jC3pUa/DojqPM/Rzc7wowDNBrTBnDip3LRhV6OKfJUnOL0TIvn52dZXJykoMHD9LpdB649FHqpDYCdZJjERc8U90Z9i/t588/9yf8z6t+h5mxaYb1MmUtKTOtOIrQsvtELCQ9sMNDrUiMllzBmD1QVw5tFDo6C3lCdKkVi6EkNSIjDoEkT4TuWhjuvPcWnvCMF/HXe+7jx//295n70d9AveMPGdx98yOj7H/qD3Dg9MuZ+/Br+cTH30cyPss9d+xnem0vntSr0f6Y3YdgIz74mOQrH1u3dg2v+NVf5c477+Q7n/50zjr7bExiGI2K9gAqyiKqMiOHRWegDSqMwNU479HOtSNHFbUf3nuUrUBFhqGrhJXpXZSCa3xtKa14QmitwEoEmQ8mAm6Kfr/PxPg4995738Py+g6HQyYnJ4c/9VM/c2e3272fJ0ExGrH9zDPZvWf3yVQA6jDwf+V/Vo8BDc5Z6mIQQ1YO+28hhHAXwbfll5ylgUDYFoIANkabL2it8d4zOTHJmjVrjw+0+CYAhDY4ZsZnuHf/XfzNF15HJxlDh1z6pWj13ciCVUBAPi8R4g1I6J240/qYY99k1wcvvgNBhcPCUkNUrrk6nowNdyBm3X3ttpv4vpf9Em/8wR9l71t/h+4P/ipTj3nKN33xb3jhz7Nvw2NY88E/4YsfeBfrznwU99yxn/5URpIpqioi/tEMpanuGlpuE90dgue0rVv5/Be+wB//4R/SyXMuvfRS1m/cSFnWOFtRlyMJ9IBWPyD8fxdf4+jW5EMkcfkWIS/LMpqHOIIvUXZEsLVMEFyUFAeRHyvVTA/k43VlGY4KAtDtZvR6Xfr9cSEcPRwtgFZYa3ccOrSwsLi4yJGPwWBAUZYnFWeeHLbmD4toPYpxR1si3Q/8+CxwfvuZofl6tx2fo/wkyuz+eJaltqxtkuU5c3Nz3HHHHTjnvvET/yZUAj7A1MQU1999DW/rvomXXfJz7FvazaAqyFKNZ4UqLM6zceNTCqUS0kTsw33MF1A0WfaCA+gggZbeSVWls0RuWG1wNpCPGVQQDYEfBpJU8dXbvsoP/eSvMdaf4Cff9FskL/gfrNtyDnv+7S8ffrS/02PuZb/Lrttu44J7/4YP/ccH2HDWBdx39zydfkKvn1KOalpJThCClI1a/BB1ADqOV5sF9bP//b8D8JiLLuKixzyGsfEJlheXcN61nZd3cfNQ8hq7aiSvazt6XRUlEKPegl/17xHPCZ62BXE+8jWUQhmNresVnEbJhpUYTVXX9Lo9+tEi/OGYA3TyDmVZ3vWmv/iLY37OC77vhTzlqqcxiM7Lxw8CqsN2gGP4fjTjApoZ/5HX11YAwBjPJJ4ApxNClwDGmENG6RuL0RBCYOOGDUxNTVEUx8moethxATGhmJiY4BNf+zD/8JW/Yc34RjppTm1tJJxEinDkjAtYGDnuQbxFQkys8Z5W9+6cx+jGYi3gaxeZZ57gGyGLAw3lqBJNeyGtwVdv/yrPffHP8qHffwNbPvTn7HGG9S//v3Q3P3y4wNonv4CJl/0huz73EV6c3cpXvnIdG866gHvv2k93LGFiKqcYVfHk923PTTNGJmC9xUcKb13XbJhbwytf+atc88UvMj7e55JLHs/W07dTl1XbojoXORfe0xDYbdxUlDKRwSmkHWttfJ/iZuOdvAHa4ANU1kZPQcQWTMo6afGUcFbEYkyj0Ggl+ENVWsq6bmPDH45JYOST3Do+Ps74xMT9HgBj/bFob3bCPIAjkH+1ihq86tGYYzZON0c8vtBq31eJhXxgyil/ZogbTLfTuSbPcpRWjPfHWbd+HSrqsE90JPJwtAQCBqX0x/t88Lp/5l1f+RvWT20mz7t4hElmTDMWjB528fipa0dZxZtOLIZFIahk7FRbjwsqBlYqiqUSX7n2JbeVpRiWqJgdEEKQ1CHn+OqtN3L+E7+TD/7tR/iR6m72fvHDqGf9V9a96H+RTs89ZDfi+LmPZ82P/T77Js4gu/qvecuLn8Db3/8RyMa4/ZbddMZSuv2U0bBqNRQNPmKto7YuyifiFASoneXsM7bznvf+G6959atRSnHO2Wdz6aVPYHJ6mtFoJJz/+H3EvUc2yIZU1KgBfZBZf11ZnLUt86+2TsaOzuGsSHyDp7UGT5MU6yy2tvH7WsG6lEy6s8ww1skkhjyVSi1JTHRufni4AEVRfK0oK8qyPOxRVQICLhw8eFKW5cnq6A2lHmAcsGpkdZTFdAMwDITe/afs9RXKjt0QzALdrrq2M+r8RFVXZHnG5o2buPOOOyhGJVmeHX+V/yACQppx3/FvAp7U5PQn4N+vezfWW178+J9m/9IeqlCSmoRh6eLhosT5GCFUqQhUEctSF3tgk4j1t3fgvWqJQ8obVGQWZmlOkgg6bYMjzwRIdGVNt9/hlltvZM2a9bz5re/ne//p//Hqf3wHX+hsIn/+zzM5OEDxpY+wfNv1p+Tmm3nCs1FnXcb8qKK++Yv84HrPn3zg79h41gXs27vM0qEB02vHSDNNMapX7NOiIMfFhdqMUoUJCSNbsm3bNm67/Q5+7GUvBWB2dobLLr2MM84+R3QBsdwP3soBpFghmyFmn00VoNt/86L0UzIt8NHtpxm/NvJtgpPUx9jvGy0nf1k6rK1RIVDVFd1OVyTLPpAEz1g3o9vrPmwjQJE1c7Mx5n6sfe8ced7h4sdfwvLS0klsAGql9G8WlToqEKFxVYHJsljbHrYJLATC1wg8bvXyj2j5eWAIQaOUfZ+19s+t9XTzjPXr1rFm7VruvOMusjw78Vb/YcIGVjYBxUe+8q/UtuJHL/tZDo4OsDRaJE8zwQFi5dOYqoXGRMWvUIllUQSJGVOILBUjKcFRf6CCpi4rtM7E+04hJxqGrJNQlTVaKw4s7GdxeYGnfNdLuPJpz+c97/oL/vbTn+EatYbRhd/J1BOehz64m+rWLzK691bc6Pj6w2xmHd3tF6A3nYednOPAvXczfueNfN+c45d+8yVc/qzny/jnjr0kqWJmrodSkvkXiGzA4CnKMo7/IqAWPfYgYF3N2rVrsdbyAy/8fg4cPEiW55y5/Qwuu/xyJqamGI2G7cnfAMY+AnlNDqBixcVHRUk6SuFihQHRu8GKZD1NEiFsOdey59pqIsSvUzJWtFUNKIqqEL5HakSHEP0BHg4AMM4rRsbo/zxqZVCWzM2t4zGPexzLyyeeJnV4tp/imCAgqzz/jtGPfy4QHrc63i12apcQUoLtkuTFfd0sv74oq4ucl51ry+Yt7Ny5E2etpL2c6OH+sG4CGeMT4/zHVz/AsF7mvzzxf5KaDkvVPFpLMSUkNt/ur5JIo4QFiERap50EnTT06oAKXrIHE4WtLTpVJKnG1hZnhR7snMSS16XFpAYIaAskmtvvuZVup8dLfvZXef4PzfMfH/4H3vuZz/Opuw6yuztHsfYckgu+k87wIMbVGLxEaTk5VZVJ8ElGbR30JimCodpzL1PlkLN2foGrzlrLj7z4J3nsU0SUtOPeg1RVRbeX0htLqGsrTMi2V7eR5hwXXxSSNXiA857JqWnWTk/xvOe/gC9/+ct0Oh3Gx8d54uWXc9Y550VvPxtl1g6TaKx1WGfFwCNunniPTgxpolFKEoedjapCW6PbXAEwGBKjV2i+IbR8Aq0lGbgoqghyyy+T5alYiztPkiZ0OylVVVNVJ8e9P5kKYFQMr6nKckkfI1ilKEYsLy4yu2YNJypSTILi8Arg8Hng4U8msqqOsQF8MhBerlgVMy6bwPkE31PBDFGKTif7mD9oL/JpSrfbYeOGDczOzLBr1256q3qYE1rXD+MmkJiUialJvnDrJ1kYzfPTT/kN1oxvZs/SvSQmISg5KUKQhFoT3zTnPcpIkm2SS1ptVZZCMEokyMI7jzYCbiVpIje4Fn66d8IjMKlBOY9SYl5qa0uCYVQM+fJ1X2WsP84zX/gzfO8P/zdu+/oNXPv5q/n09Tez0+7nlj072Z1O4LMuNQkh0ki1UqRAp1zkjGyJMzeu5+yzN3HVVU/m8Vc+he60+PnvvOcgZV2R54apmZwAFEW9qr1SKyU+K4Bnw/HX8WQe74+xYe0sP/mT/5X3vudf6ff7WGt59HnncenllzM5Pc1gaVmyF6LSL1EJWgcylUhCgxevQIK4Ads6MDk5ISO/qiJNY2Sb95hY6DrvqapK7MKUak9zpRV5nlGWJZ2OWIPVlaQLW+sxSUoINcPRCKXy1tvyoe7+mzHz1PTMx5vJxpFXVZZ0e70HSug6zgqgKf3VMWCNOFfxTejC/X/YR4EaSENzCMomMEmwFwXX/VzwhixTH52envqFoiipa8vU5CRbTzuN/fvn7zcSPKFq4GHCBUIIGAzT0zPcsuMm/s/7f46fetqvce7GC9l16B5BYpUSdlmTHhTzA5JURkrFsCTLk/iatwkX+CCWVhBYPjQg7+ZoLd74SW4EEPQe7RXKRAIRYHEkkbhSFANuufU2tNFMT2/keS/9BX6oD8WhZXbv3MnB/XtYXNzHwuJBXF2hjCbLO6xdu5a59VtYM7eW/jpxIvIW9s8Pue9rO0kSTZJqxidSlFbUVUzZidS70C55yVBsbL3VKtZoWddMTk6xcd0a/sfP/wJvfvNfMTExQVlVzK1dy5VPfhLnnPcoyqLAOrtKry8BoN450iRp79GGWRh8AKNYXFxs2YG2rkFJ0pMQZwTsq6xsRsYYuUVVQKGjdbjDaGEQWuXodHKsraOPYYIqS+rak6SRyvwQjwCEAgzLi4ufDce4r6uyIElTzElmFiarEjdXda9HHRhGAYaFGKV0xDVPCF/xcLFqvldrvGGfjEs+5+oOSTb8cJoki4v10kSaJGRZzqZNm7nr7nvYs2cP3W73wR3wD0M1EKLGfGZ6lvnFvfzh+36Bl1z5P/iOC5/HgcF+FkcLZCpFGxONVhQmlRKW4EjzlKqy5L00ctY9JtI9nZV2Qac6VgDyflSFI9MZeS+LAZlStupE4dE4HzCxTUskpYSDB+dZXDwoGXhJxvjUWrat2xrDMTQ6oXXUdR6KYcXCYMi+W3djnZNMAx3ojaWYTHz4q0qiz1gFxiWJjm6+4KIO37kVpD8QKKuK2TWzbFgzy8+8/OX8xRvfyMTEhBh2JgmXXPw4Lrv8iXS63XYhi05a3k+tZGLiI8iXGEMdTUAbK29JBvYC8BndVhBaSyKUin4Nxpgo027ovkJDToyhdp6qlDYgyxOZ/ZclAcVYv4+taopRseJG/BBeWuTPS9bZT2ljjrrhlGXJU666iq3btrF7166TqQDUygJQD3SIxg3AWikdj27v/VkIF0e4pi1JAjw9+OSPlE/Q2tfB+/fUzv1ot9slSVLWrFnLltO2MD8/f0xi0AlvAg9DNeC9Z3ZiLUujQ7zlY3/KbXtu4kee8t85bXw7Ow/cI+GjaAwKbRJ5yZTMpNPU4OL83wdQTeiI8iidrOjNvcd0EqEM1xZbqhhmEtAmaTUJ3jvSLMWkCXVZidllqtobyfuKQwfnOXhwPoJcRso0paOPobzFaSZa+SwXzEFHvoKrQnuPOCfstIYVWkY+utIaV7t2+tFMjMqyZPNpWxgf6/HiH/5R3vH3b2N8fJw0TVhcWua8c87hSU++ii1bt1OMRm3MlxB9iBkAK6IeHSsO0e8Lm7AqqxV/+8biWyuSLI3eCzXOCgDporlrnqdCvY6ehEROR5bJubg8GApRSwsxSDlLmqZ4H6jq4mEhAWitPmCSZKgf4F4eGxsjSZJVArOTGAOq1dOAcLSwUNrTjIZUcf/rvcDPhiPm9YFwBdRjwacD5zSTE2PvG1XVj1pr0YlYMm87bRs7duxg185dR60CTmpdPwzVgPOWfmecOq349E0f5o49X+NHnvrfecz2y1kYzjOyi3gPVW3bqOokUWijyDtpvBkj5VcLz8I5JyizAq+EbJQaAeucD+AtqUpiDJbCJCI6KoqarEnWcQ6rDVr7CIStEskYjTJA0CSJjjRYWdRarVQ4deXQSfQ+VKsU40oJ+KZVO9tXGnycq6vGMsE7nA+cf9457Nq9h+9+1rP4zKc/xcTEBHmes3hokdnpaS699FIe+/hLyPKM+f2LQsYJKmb6KRIjP8fGSQJxKqKNJjEJaZZSFDIXV1pFEZDDewlptVacnRIDjYJX5MKB1Bg6nZyqrPA0E4CV8A8VnYK0lpHiqCzJk5Q6thUPVVqwnP4Vg+XBPx/rZzSbwr69+9oN86TGgIHDQxk5VhugFME5wrFlh1cD+0IIaw9bTiH0wD/eVtkndJbQ6egP9nrdeu/8fOpDIM9SZqanOX3b6RyYn6eua7IsOyao8UhrCXwEB2dmZth58B7++F2/zDMe93xecOVL2TJ3JnsW7qWsS7IkI8nEFDNLpJcuByVppiUvL/asaKitBaNJtYiCgreYJCXJE9IsWfHRdwHrLGmWgArUVU2SJnFe7gleE1SkcGuFikg8TnAK7Cr9plK4SAIziYwlnY3AZGicNaIgSgnBxscSvTGKafIUqqJkYmKCLZs28LFPfIqX/PCL2bHjPqanpzHGMBwOSbOUCy+4gKdc9TTWrlvPcHkQffhWNBRJmkgFQ0MFXtFPeO/k+dY1wXuMNiItDpFZifj5SZUDdW1J0gxjpGWxdXwtAmAUymvKqoqApZRELoaHBiI7MARGRcFg+NBmAzjn6HQ7RZqkHzxWrLq1NeNmgsc9/vEsnQQHIDIBDw8EOnwmeP+Hasc6x3xcHWnArfglhIAnPMFZCL6D0nbRaPWONElaiW2v12PbaaexcZMEP36jAJETqr4eBI34RBBb7wMzk2sYGx/jI1/6V377bT/DJ7/yQdbPbmb9zGaCEv57moq6bTgoSFLdMieVDhJq6T35WA4aqqJqSdp1VcnJ7n1LsAkhYH20I1s1J2+fUyybm891zhMUkbrspf/1/jCfBxHr1NhowOGdPJx3kc8g8t26JfnY1pLLWUs1EoLPlk0beNWrXs3Tr3oyO3bcx9q1a4W4EyWt27aexpOfchWPufhiqqpkVIxIE7Hc9iGQteW7VBZGa5J4+EhlIPTfqqji10jFIN41AaONlMbOkmUZaZq295aJuEBtPWVtJRNAiQxbKcmMaPALH2S6UVaV2JKVJYPlgbRgD9FlrSXR5t1r1swsToz3Odqj28kZ7/d59AUXMDgJDkBsASJ7tWEAKI5R/69sDCvgzlGv9wE/tLJ8QnMzfh/wR65K8V7TH+u+dXF5+BLrYpAjMDU9w5lnnMm+vftYHgzodbsPONp4OFqCE8cFHJnJyWc67Fvazev/5Xf41A0f5Huf/BIuOusSRnaZA4v78Cqy0LSMoZx3pJ1cQK54AjSYgHWOLM0wqSyOUIsFlPSnirybUhYVLngZQRKwtZVTPF0BGYP3pLlw2KtK7LFV427uxYtAo8SuW4FDYXQjeKK12iaATkz8GgEtnbfUdcXE+ARbz9rOV264kV/4hV/gYx+9mizLmJycbJ17hsMRc2vnuOKJT+SJT3qSLKrhsAG9xIYrtpvKR3vuVGy5y9qKc29cJGLU2qT/pEL5tTZGfwtQp5X87jpJ8FVFWZXoeLorpWNFMkJrQ97JqKuKupbNT9FEjjsmJyextmZpecDi4iHS9KHZAJRSJGnKwYMLf3Vgfv6Ypf2oKHj84y+h2+uJB+LJVgChpQI38l8e8KGDx3M0L+AAhH8GPwi0NjqRkx0uQbnNtjR4l9LJ0v+YmpjYpY2UuGmeMdYfZ8P6DWzbtk144sfpvHrC1cBJMrJO5HO990z3Z5mYnOD6277Ab/2//85r/uHX2bX/Hs4+/TzWz2yUsR4epQPKNN50oR2vqQblDy5OCGxrptk64VS1JBGlZiWzIQpuXGNlFI1JfBAUv65tW3XoxqQkavNDVNX5IBLlJmiztUBqgjyjCYx1niouqEedfx4zszP8wR/+EZdc8ng+9tGrmZqcZGJioh3PjUYj+mM9HnPhhVz1tO9g3foNLB5ajOGqAjJ08pw0TSnLMgJbAup578izTKqAuJk3ASBND+xj1dlUPjZyEoqywDkni7Z1BTZtyEcTSeacI0mk7QBFkproQARVVRGCYjhYZmmwfFLc+xO4T+9Ks/STWadLmuf3e2TRR+OxF1/Mhg0bKMvi5DcAjjzj1LEfkeRKEwp6lEfhQ/gAoe3/BW32DpR/mrVgyxSd4Dt5+mYAY4SIopVids1atm/fztq5taLhPt4YsROp8h8mZaEg1obZmTX0xnp88rqP8Mtv/Ale9be/wd177uC09dvYvH4LWZ5hfePuEtCJwmRG3p04vipGBUmmSVPdhrQ0C7YsKurKSsR47aijjTVKCEbOOnHejZbSDTvPOkdR1W3ysbWOsqxaj8PmZzReD00Ul9idizDFKMVZZ57FaVu38u53/xNXPflKXvmrv4J3jrm5ORndRXS6LEu01pxz1lk8+alP5dxHXcCoKGJASJytEyjLiqpy5Fne/lsIMYxFiUTaxnZErLxlUjAcFZEKTMtDaN4uH1SkIRM9K6Snl43WoLWhqm1bWTSsRVs7OllCnmqqYsSGtVMMlpYYDEYnpb8/gZby9W1UWuNpuOrhrORW1HVNURQciyV4fGNAJbB/4Agk8BjTQEnKCRz7R4Z3BXghRwiHQvDPDoG/9ZIbTifv/HWn0/mNqqoYVjWBmk6ny4aNmzjrrLNYWlymKEZ0Op3WYvuUVvkPE4PQeUeedOjO9hiMlvnw59/LR6/5N5544dN56uOfyWMffSmbN2xlYfEAi6NDMfK6bimsaZ4SiHHYBoL1qEiHTaMrrDDTNCbRsZrwEOPOCbKXmDRttzAfJbUK1ThDRjqviuQdCeYIPkTZrDR9dSVOCFNT02zZvI6FQwP+9V//mTf95V/wsY9eDcD09HTb6zeVZV3XWGs5c/t2rrjySp581VUi0Iklv/dBxnLBUZc1SSpSXG1MKwhKjKGqaxKtSeOitdaigrj/GKPJs5Ta1jLB0HKCV1Ul3P7axk1ZxzbIRlJQI9ryhKBb/ESwBtqRd5KkZKlmz57dVGVJr9s59eW/Vnjr62JY/LWoSI9+n5VFQa/X40lPuYrFxcWT/nkJR+QBKlbmu0df/woToH5gKuS/QlgAplYYYkAIL1DK90dLLI+NK7odc/fkWO8jO0ajZ3TynCQ6Bk1MTHHG9jM5cOAAX7v5a9R1TZIkD90mcJKcgROiESMU2W7eY6zTZ1QO+dSXruZTX7qa88+8gCc9/hlcdtEVbNt8Bmkn5cDCPMPRklBZvWJssstgcUSaJXE+72OfK4w8o4U+HIK4EutUE4KMtMTzTvr/uqpab3z53TXehahelNLaOkHRicCejYnIvV6XubVrmZjocudd9/L617+Rt739bfznf34OgImJCbIsO0y80yDaZVmyedNGLn78xTz9mc+mPz7J4uKhOHo0eKzM2rUizQSsw3s8SkI/tdh2h+CorEUrRZ6JgrSqapSCJE0jYVWmGM7a6FbtMUq0/t759rT03mKMFoZgWZImCWUtqUNpkohDsVZ0ex3CcERVW+7dOc99O3c/pCPAoizeUpTFgmgN1DHvpzTLGB8fxz8Ib8Jk9aI/cqEf61bWcQ7s1TFrhRr4u0D42bYVkMWWo8OzyxHvKgtNr+8YH+v+YX/Ye4bMbcUdJ88z1s6t44ztZ7KwcIgd9923wt4KYRVecQoP94cBIGxOFhccWdqhO9OltjVfve0GvnrbDbztvW/isouu5NLHXMmF5z6O07duxxjDoeVD1JSYVEZ6LtKBjdEEV8cT2lAVJTqRqQIuoGPiDdG2bLA8EG2CYmXObWW8FmI+njEqgnWeJNFMTU4zPT1Ft5Oze88ePvHJj/Le97yX973/fezevet+C//I6U0IgdGoYN3cHBdecCHPfNZ3sWXrVoYDGfkF57HYNuCj4RmoaLZinYvgpIy9fKQ/e+WFqBNl2MFJ66AI5HkOOBzyextt0EaL4MwkbTRZYjRZKptjFBcLhyCIl4DWWub9sWVIkoSyrNi3f/647sGTBJtIjPm9sf7YA95Xy8tLPOu7votNmzax6yQYgIdNAQ5r/tUDNwHy1jQW4A/YLfytbAAhrq3QcOOfj+Zdo4FmYtqQZ+Zj/V73lv0HD53d6+YQFHVdkeU5W7ZsZWlpkeFwwMLBBXpR9NDcWMezCTzSiEOrv8p5manPTM8IOj5a5mOf+xAf+9yHmJme5XGPupTHXXAp55z5KM7Ydhbr5zajtGM4GjAshpRVIfPvxIBugjVlFOatp0by7PIsFROS2hESIX05BzrR6MgENEaRZSlT09P0el3yLMPamnt37ODTn/4En/jEf/CZz36KG2+8sX1Nm5n+0RZ+cw2HI2amp3jU+efxzGc9mwse81hGw1FE2RtgWai+wQtHv+n5ZdnL76BMEh16Gndq8QQ0SuG98CDqyormvyhWNpIoQIKwwpSLsuyxbkZnbJxytIx1jrKqMYkhU0nEY6Q1mD+wSO08xiQsLy2zb9/e1hHoFMP/lGX5L91O5550lTL2aG3C8vISWmvSuPGe7GaUqCMWv/rGGxRGGRKtGdmq7aeOcl2j4IZAuACiGYR8/HuUCZ2lBVuMTxqmZzVT/bFXLY+Kv+p0utSVRD51s4zZtWs5fdt2lpYWuemmr1IURYsHHO8m8EjFBQ4HC0OMneoz1pVwy6XhIld/+t+5+tP/Tt7JOf/MCzjzjHO58PzHsn3bGWzcsInZ2U1kmYRbeAJpLqdbYhqrLDnddSJjJYA0S0gSI6SWSPhBKcpixIGDB/jqTV/hnnvv5YYbrufW227hy1+6ll27V06Yfr9Pt9tty/wH4msMh0MmJsY55+yzeerTvoPLrriSqpZxYYPe0wZarGaOipNwkggtOQQj47y2JA7taJKY36iUIskSfBVVqCFI6m/cYFyk8UpFoDFGxd8/Jc1ysrLCOcEYVABtkjairKgEnM2zDnv37mXfvv2xyjilBz+J+BP8RlVV0cLsGOV1XTE2Nsa5553H0uLSg6pEkiOEwMf1RFcQcfeNPvfVgfDWNhlHKoc+8CJr3VuLwqMSRa/TefP4WO/35g8truvGF9YkGpMY1m3YzBmjIcPhkFtuuZWqqg5jCZ7IJnDc1cDD6Dh02OvaWI0D471xGFPRBnvEl2+8hi/feA3ves/b6HS7nLZpK+vXbWRyfJLpmVnWrV1Hvz/BxPgEU1OTmDQhSVK52RPTYhAhOIqy4NDiAgsLC+zevYv5+X3sn5/nrrvu4N4d9xw2U1ZaMTk5SZqmLVHosFi3B1j8/X6fs886g6dcdRVPffozCChGo8HKodH6Iq7yj6qr6K6koxGonOKSGuRWYVRCqwaF1kF0/AhZSGtNHT0JxntdlNEsLS1LtdNYk/tAlhjs6BCD5YK828Wklvn5IgpZRT+gInbgnCXvZNx++x0sLS0xOTl5qg9/vPf/apS+ydYyqj3WXTQYDhjr9Tj73PMYjUYP6ucm7e2n1HHtAlJaBZJolvgNxnTvCiG8AUJ/hRQUCIEfSfLw1gP7C3p9w/RMh6mJ8T8aFOVrmjy24bBAJSm9yQm2nn4Go8GAoiy46867JTc+3pCrEdtv9WpgdUfVGq8oRafTo9vpoRDCUFEW3HLb17jltq89IJosaj4tX9em5j4ww7LT6dLrjh1GcvlGJ/39Fv9oRH+sL4j/FU/iGc96DkmWMRgM0M0YLvL5xbnXxDRl126h4pSkWmFRYsSJWUVikncxgs07tDatu0/w4LVrQ1zLqo6YR6BydRRZNQxKz7CEqnZUbrk9+a2tccGjjbQCiVEYk+DqmnvuvafFB07l6lfAYHn5lxtyFTyw2+D5j76A8X5fKOMPdgNomIDc3x7o2OVKFKOIj/oxP3cE4Q0BXsHqVF34DqXYNFyudgyWO6zdkNAP5nXj/bFXHlg4tKaTCIVTJykBGJ+Y4oyzzmFUFtRVzX337ZCSL04GVt+oj6QpwclUA+HIVz8ctnWilKLb6dLr9u4HtjV9sY+z+tVYTZpmUdAjvoUNcebIG3m19dbJVJZC9BnjjO2nc/nlT+Q53/MCJqemWVw8JM9Fa4IX3b1OUyEIxeoEpUkjMcc5IBp61JUlz1PyPGFUitmHs4VMSGIr01h5BSR/II33RlXb5tAhTRNpC5Ax6yhmMopbkKKOm0VikpZd2Iwz806HhYUF7rv3vpPW3h97/StGg8Fbq6K4VX2DjaW5v7efcQabt2zhazff/KA2o0QddpuuygX4BjFhYRUZ6BuUDa8NhFe0t3dkkgUfXp7m6pX7dy/Tn8yYXTPm5qanfn1YlH8RIkc9jaEPPnimZmc55+zzZIzlA7t27ow3sDm8f3wETQlOVTVwtJ99tMpLaK1ifXUiU4lTtfcNhkMmJyY4fdtWLr3kMp7zPEn1XV5ajCQc1Zqk1LUlMcLtNyaJ7MnIOgzS/zsvMWp5Luy/sqojrTe0G1lzkjebX6+T4YI4+XjnVk78SHzKjGFieoKqrJifP4iKIKZGo6KKsJMlZJlh/8ElXJySGK3Zu3cPO3buJO+c2vl/YoxXSr1CJ0nrIHVsTomn1+tx9tnncGB+/kFXIrpl8xyRD/oAZEAIgVQndJJcCCcPfO0ihLc2/WMb3kh4sdaKwaCiGFiy3JBn6Zsmx/t3SbiG9ICN536WpmzcvJUzzzqX7aefzrr16yjK4pgI6PEwCE+YEPhNDC49WfbiKQGowgOfSCEEhsMh05OTnLFtK5deehnP+97vY2Z2LUtLi9EIRTwPpI2JCT4R8/CNwacXF14VpbcK6HQyYRHWdduKhNAYgTZSdtn80izFBSn7XZyGNMeb1k1uQE05KqLCMpCYBKPklG9A04AStV8IpIkhzzukacqtt9zGgfl5sSI7had/URS/prXe0+12yfL8AR9NduQFF13IYDB48JvPyh12jEzAB8ACxGftuOKRXhlCeGmIQvHYn20Nge9OU/W+5cWS0UgW+/TExC8NRsU/pUna+g8abUT6GmDb9jNbzbz3gX379pBnnaOGjX67TAm+UTXwcG0CR/5YpVQbu7VmzRq2btnCpZddxnd/z/OZnl3DaDhsgUOR56r2wGgWp3MefEHlV4JlGiGTjenBPtp8ezzO2YhnBLSR3r+qxdtPANPqMI6+1hoVpOQ3RkwRFhaWokejoSiL+DPrOIRPhB4drcSr2tJJE5RS3HSzZDEapU6JH6DSGmftfUuHDv3h8VStDfHr7EefQ398XFqWB70BqCNHgOq4xgGeQJ7ksf9y3+jm3hkUf69CeHHzJkedwCtMqt533z37mZztcvrZcyRD+88zkxPX7plfuLjXyVehw1buQpOw7YyzBP1VwpPfu3s3WZbfDxP4dpsSqAf580/lJiD+gyXBezZv2simDRt5whOfyLOe81zWr1/P8vIyta1jso6Urj7mqiulqJ0XyzjvEdoRLRdAGH8N51/m702qcKcjGY2DQUGaaCFDVQIq2sqSJilaq1YEFCL4aRLTbjguSqjzRKF1QlGUZGlKUCtWa3Vt2xbTOc/tt9/OnXfcSd7tnjIz0Ohd+ONpkoh93HG0a857nvzUp7Jubj233Xbrg24BEtpYinCcg8BV/YNacbz9Rk9EBX7Fxw1g1SDxyhDCOWmmvz6/b5ENW6ZQwPRE/6eWhqNrqqjTDj6QpTLjts7RyTucec657dgsNYYdO3dIykuWHnUT+HaZEvBNrgZCRIsHwxFZmrJ58xY2btrIlVc+mWc869mYLOfAwqE2pKPx9vPR6k2irj1EdZ9WunUYEtmzzOqLshT9f5a10lxiqa6jn4FzNVUlysUsTaI/gXyP2snHW1KRTmLFUqO1IYvGLM47UqNJ0oTF5YHE4OmEPM+oK0vA0+t0uPXrt7Bz5y764/1TtvjrqvqQtfbDOjHHtfZETKW46DGPZd/+fadkEpEcJjg4vklgu6jSJKWT5CxUh8hU+oBziwD3BsJbgJc1XnERpf61LDcv2XH3fvoTHc559GZs5a9dMz31j/ft3fcDjbdbQNDaJJExDSjOOOvcGNctRJd77r0HVzq6efeo2MS3w5Tgm9kSqJi0MyoKpiYm2LRpA1s2beEpT3s6Vz7lKrQ2LC4urkyUdcPlC1F5Fwg6eg/IShNCT2TqSYlOROI1Wklcuo7hKCEGstbRGkzrFOsqOnnaegMQJG+web+1knRra100BjXo6A0Woq+F0gmjopRqwsfRYQS40zQhMfDl667DOXvUKvNkXkelFHVZ/ReZwOvjvNUCFz3mYrZvP4Pl5aVT8p7qFeDvSBTwGz3ks3tZr3WXCd/gPwiv9N6JlrCxwvb+R0MIc3k3Zde9BxgOatCaiX7vZyZ6PduYOzTxgY3Ms9HLn37m2Zx3/gVs2Xwap5++nSRJGI4GD/giHi9AeMqQsocIIHywP/tEb9oymmmsn5tj29bTOPusc/j+F72Ip33HM6jqmuWlRVmi8eR1tY0GM9GJCKJU2beeM5EAI4BggKKssHUt9tzRr1Biv6Pe3/loWCJTAWM0/bFu9DXw5HkHF6LSr/2+snXYupbnUAvbtPa2ZUyGECJT0MVWQTwIJ/p9du3cyc1fv4XONzCoOa4FpzXD4ZC6rn9FK7VLidf+8T2Ay594BdMz09Hq/BRMIFRDq1Ic//Hf4ADBk6Vy8rtgj2cn2wW8Ovjwv8Cv0mqHX0oS/YpDC8vsuGcf51+4ldFyeWDdzPQvDYrytSEivc0mo41GBUVta9I048yzzyPvdMiyjCxNue+++1hcXKTb6R4VHDzeauCRrSU4oiV4iHCBxmRjOBrRyXM2b9zAurk5zn/Uo3nGdz6L004/ndFoRDUaobQmiSq6JEkwJsQ8vogRack9EJsvyfLzQZJ9VqsQrXVUtkYHLTbf3pOlmQiGxMw/uvlIubC8NIh8f3HJcc6RJQk+ZlM1+YCNoKyKCcImScF7yqrAaM3y8iASkoRs1Mkz0IbPfPYL7Nuzh6np6Qf9etZ1zdatW2+89957/mgwPHEU/8KLLmJ+//5TRkRKjkT2T/TG1UrTSXOWy+XVwoIH+qrfCiH8NITxaC1CCP4XIPxR3k0O3HbzvUxNjTG3bhqjzevWTk/+xO75gxfkifRwiTH4uGMHAmUxIs1yzjjzHLqdHll6HVmasWv3bvbt24sxRowlTnIT+P/zlEBMPISXPjszw7q1a1i/fh2XXnY5Vz3tGYxPTbJ46BCurtHRRcfFPlwHUdYpJb17aEw+TeM6pVZi5K0FZaJ5iGsXIS6QpAleByrrwTk8HqMTvBKjGecCpa1Wsvq0IlHRUdkKXd07kTo3I2MhD4nBkYuWYcKh0CIDtg4dN6alxSU++7nPt6SgB1MBNJmE3vkXGp0ct6GIQmGd5YILL2L7GWewvLx8yjb45H6OoOrEbto87ZClOdXyAXR6XLvSCPh5D3/dhDmGENIArzCJfsXCgWX27l5g67Z1HFoYsGZi4sVLg9ENo6oiT5LoUGtwvsYojTMKayuWB5Y1c+u4+PGXM3bzDWRZxthYj127dzMYDuh2ukd9A0+EOPT/lylB0+sXRUm322Hz3AbWzMyw9fTTueppT+f8R1+E0ZpDBxcEyGvy9qLtl7DnHBa7wvsPkhlkrXD4tRLRE2HFTVhMaaRvt3hJ44nAXcNWDwGss61HIVEN6Z0YpiRGJLtVdP/RKsqmQ5DwEiteiNoYBoMhRinGeh2xSoujyLoWf4Eszbj2C1/ktttuYzyGmJw8eBpI0pQs8Mu33PL1r+VZTic7PkJR48z0lKdcxfTsLAcOHDiFFUDjBxBaROCEbj/nLXmStZbMx6lM+hsIP+cJFwXfUF3DL3rvXzU2nu2/84772Lh5hrXrZiiH5Y0bZ2d+5849e3/TBY+rold8kkLwaC99lbOeqi4Yn5jgosc8nn5/nDvuvJVed4w9e3czPz+P0YY8/+ZWAw/0M1ptgwqrGFonfqOp1T+nwWyO4+ZtSD1FUWCMYf36dcxMTzI7O8uFFz2WJz3laazfuJGyLBmNRisee4h4Rmkddfdi46W0iHiMEWcf41f8B23w7Y2WRncjWwunJGiNUY1FdxCmYJCMAaVVW+6rOFJsuCViaCJqyNQkK7HktNEGYm0YrdHSRMJEi7KWTAGlqKwVc5UkRaP47Gc/SzEaMTY29qA2AG0MRVF8qizKV5lISqr88fXxtZV5/wUXXcT+fftOqQ4hWQ39n8x54YKj1xkjS1KquozU3OO6fiyEcG1z7oUQkhD4I631jw+WB9xw/e086aoJglZ08+y31kyMf/+egwvn50mCjk/WB9oASFGWBdmQuh3Oe/SFTE5N8/WvfZVOJ6Pf77N3714Gw2XyrPOgOQMnOiUwScJgMKAsjm3eODk1hUkSFhcOxUnHiV8TE6Lcm5/ff/I3q9Js2bSZ9XNrOf307Vz55Ku46DGPE+ecoiQNHuI4z4cVG3JQ+Lomz8TSXBB82oBQY7Qg8TKfiyWwwqQp3jm8l985Mbq1WRcCkSfLRDdQW4eOo+eVnr6O40UY66Qo0kgW8ywsDQWP0IGq9u2eGOKhp7SmLEqht0emoVaKNMu5/fbbuOaaa+j2eg9u3FfXjI2NjfDh+xfLQ/HgdCf0fc5/1KPZfsaZDE4R+r8CAq46+Vc9ayAc94mTJTm9fIzlYpnOcW4AAb4UCG8mhJ8IUawRgv8xH8Kr+v3u1+69ezdfv/kuLr7kPBYOLrJ2cuIFg1Hx9UFR0snS1qNea41RkhMn+XqBspLdfNPm05icnuH2W27m7nvupN/vM79/P/PzBxgOh+R5fj+Q8CFpCYByNGJ2ZoYzzzyzRb1VVIGlacqhxUVuvfVWquGQrVtPY/v27aJNXz06aghbh7k3Sz+dpilFUfDlL1/HcDjgcRdfzBmnn86oLFBBVHUqUqyP9pyb32d8vM/MzAxpmrJm7RxnnXs+a9bOMaxKlhcO4AgUVc14p8NEt0sdQzIDrbUgVV1jEtkglNLoqB4NyCQBFdl0kRBWjkZyt2ktG0YTLBorFx8kGYhokKmU0IpF3gypSTBpQlVVLC2PSBMBGysbOQG2FhOQ2AIaraUKcRZfCTiYJmIKSnxv0tTw6U99moWFBWbXrDnp0z/EzItiOPzBQ4uLe092oV5x5ZXMrlnDgQPzp7gCWF32q5OBAuUMH++OM7+4H+vtA5mEHHnTvZwQvjfgZ1YxBF/vgv+Ofr/DV2+8nfXrZ1m/cQ2j5dEtG9ZMv/z2HXveYGN0lotKLq01xou1k8dDUGg0ta2YmJjgsRdfxtzcBm697WZ6nR7jE5McmJ9n4dBC6y9wtI3gVFYDg+GQq666ip95+ctZOHAgJtOKZLff7zN/YJ5f/MVfYmlxkac97Wn815/8SXbs3Nlq3o0RUI0GxNImhmPIbLvfH6OuLS960Q9y4MA8//2/vZznPve72X9gvk28TYzMwLXSKxkQ7e8RjTaCZ1SWVLWlKEsOLS+z5+A8g3LEqK5ZGhXsWFhgy9Q0j9uyNZ6YTUShtAHOe3wdyFIjCUbxJBfRTXSVCpJopJRBoUnMimtvCMS+XcWIsZXkXhGSyWbRzPElRFW1Tr7BB5aqEdroGPiho7bfx5Hkykg5zzsYBUVVRZAQet0e99x1F5/45CcetPGH954kSf445Pl7T0RSvfrqj49z5ZOezO5du06tDPnwKYA64q/qhH7Jbt6NwZEWjr8NqEIIPxwI/y4/zeN9eHoI4WnKqI8Nlwu+dO3NPH3mMoKCbpa/cdOa2Wfcu2//85uJgPOeoFwMq4zhbwqyLKGuHWVZoI3htG3bmVu3njtuv5U777yNfr/P9OI0CwcPsnDoEGVZkmVZyyNv+AKnchPI8lx060d5/fI0a9/cNJPYMNpq5Ij3I6zKcYy8DR+kkkgispznWZsdeORIr46ldmPvXRQFZVnirMPhW1vwylqqOHFpE4Hjd6ysxRhDEjzWC2GmAQFVzOxzXui72ugI3IkDUG3rmPYrJ751Do8i1ZpOnuJ8EBvzmP6rotGHbzAS70BL+e+sRHeLhNij8ARtyLIM5yzFSKzIlVbY2kaqMXQ68l4476icbHpaKTp5Rq/b4eqrr2bf3n0P6vSP1dl/zO/f/wrZbPIT/vqiKHjSk57Moy+8kNtvu+2U+xAmqgkCPQkewOoKwJiETtbl0PJBVHJC3+SDEP7Wh/CSRsMeCH/tvT+9P9Hlzjvv48YbZrnsCRcwP7/A9PjYD5Z1/fX5xaWtxkjUs4umCFprgjIi/rB2JaXWO8pqRN7pcP6Fj2Hj5i3cftvXufeeu+j3+0xNz7C4eIhDhw4xHA4xRm6gEzUhfaCWIM9zbr75Zt7znve08VRN+d7tdtm3b19bjVx77bWM9cYYDAaRqdaU7ipGZevIcJNFpJSi0+mwvDzg4MICSZLwr+95D3v37GVpeSnagAfh0uc5/Ylx8b63lm63y+yatczOrhFjz7j52QieaTSZMaiQozND1+R0koSpvNNyMhIVorFm1OWjqIPo8HVk1/nGfhux4Uq0lnizKOwRLoknKEkZKosSFTcIiS5uvA2iFDjmFkpr5PE20MlSKivBHhJvZls3IbEMS+lmGaPRiKKQBOW6qrDekyXiK5DnHW655VY++YlPPijZb7x37jNaP7fFFk7w9G5s1R9/6aUMT4Hy76jP89Wv/MtFYPyIMeASsCn+eVxbQDcfY8e+e7l9xy30Or0TJKeFJISwI8AcbUad/2WUelVdS0jDdz7zCjZuXMPy4hCTmDPv2bv/lqXRSOVpEsvAVS9wWDGY1EZsr5sbJ00zIZV4z4H9+7jttq+zc+e9LC8vMRgMOXToEIuLi4xGozi6SUhMckI779E+1RjD0tLSA6YdTU5NkSQJBw8eXAWsndg1Pi4OvUcDAY02dPKcbq/L1NQ027dv54ILH8M5553PzMwsSZq1wZiBgEnEQIMgib95lsUEY+H3V7Yi7/YwxlAOR1TWxRaMdkNuwmcTvZKfaJKktQIjevetuNKEttJovPqNkTl9o+gLSEit85Hh5z0qeJnhByHbpKYR/zipPoJsMiY1aBRVWWDjPVPXtch+s4xOp8cbXv96PvCB95/06S+vUahGg+F5JknusJH9eKKna1kWPO7ix/PaN7yRfXv2tNOPk7ktgB3xz9XXUnIk4NdWmyf0eyvqumS6P00371LVJYlJj/vFU0rZQHh+COGz7QYAvx+8e2uamr0LC0O++IUb+O7nXoXJDN762zbMTL2o2F39Y1XVdLIcpXwLlAUkaqs5+Vay3zUhePGdU4qNW7awbv169u3dw9133cGOHfcwMTHOYDBksDxgebDMYHm5TSgyxpCmaXtyn0h6sXPuMFfj5vnUVcVgOERpzdLSUvx3df9vcJyv5dLSSkiESVImxscZ7/eZmppicmKcyalptp62jfMe9Wg2nXYavbE+dW2pyhIfXHzuniSVyDZPIDEap2FQNRr5BBcisGdriqoSFl9i0E5hg2tbmBXNv4lVjF/Z3OLoziPJwz4GkUpmoYoaASjKmvF+jzSBUVG31F3CCp7QRJmpVapW3VicK413Nd28Q+0co6qU6iBOIrQR85G80+PGG77Cxz/+MXoPYuwnSkb7XOf9HXXUGHCSJK9LLn0CvV6P2tpT3v+vwgAUh0kBTqIV8MHR746xfnoDd+25kzTaT58AkeVzhPCr3vs/WFUV/K219bPGx7vce89OPnr1Z3nGM69ksDwkS9N3bVo7+3t379r769ZZ6QOtlR5YIUxjJTFKWZaSZ7nEXsXsAe88S0uL9Pt91m3cyJq5dZx56Dx23ncP9913Dwfm9zMYDilGIwbDIYPhkNFwRBlNSLSWUMlmFLV6jv5ALcHq10TsqBRzc3Ntef9AJSXH6tJWZeTlWU5vrEe/Lwu/0+nS7XYYnxhnw8YtnHPOuWzcuAEfGnq1pRgNSVNZoCZJsXFO3vAH6sYVJ7L9AtHPTxkSbXA2utiuZpUHkPhTATpjYkf0KIzAXQhoAs6LE7AC8k5HWqRYtocQSLTCKBEBWesOawUa+zPdhprI/69DTYJpzVaNMfFzJWNxempazEGKkciStdCL/+Vf/oXRaMSatWs5GdBOa01Zlj/tnfuwLNiT2UQUZTlifHyCK550JXt2735IFv8KEzAcfvyfrECl9paZyVnu2Xc3ta1O+EmHEP4QuDzA98T675kh+BeFoP5hrN/lllvuYNv2LZxz7ukcPLhIv9f5jY1rZ8/aOT//IhPHO02/pZQAUY2GutlBpe+zZKmAVEVRRPaZYWZmDevm1nPOeY9iz+6d3LfjHvbu2s3i0iJFMaIsSkZFEXvIgrIqqat6ZWSF9KbNpqDiqGv12l59qCwuLvLd3/3d/PCLX8z++f1CpFEaye4wKKMiaCh/mlhSywRAPPKN1qgos9VKorHy6Fjjg+QOdDo5tfUcXBywZmaSTielGJUt97Dfy5ocV4kw94no5r2n8p5hVaF9YMLkAtDVglXYyBZsuP5VWckmkiZx0Uf3Hi0TFh9HhvIaKcm4i2W4dWLQYa2NAafNPF7aksXFJepIBCJiIFppnKsJyJSgk+corRgNy2gsW8X3RSjrpZXcQ2MEsLTOttXEmukZPvC+9/P5z3+eyampE178AlVoQP22MeZNxHbnG+zrxyZzKcVzn/d8zjn3XO6+666HLIWoNQU97Fg5yR/mvKeb95jojbNvQZx6TmQviQSNFwXvb4WwObaE/88H9yGt1UKepXzkQ59krN9j46Z1HFpYZM3UxA8677fv3j9/SSdaJmkl5BJxb5WTraorkQ0bUZm54NtQC2GfecpqhHUpWafLtu1nsfX0MxksLzK/fx87dtzD3j17WF5ekkqgqqjriqqqqKo6/llia0ttJebKh8YFefXM/vDfee3ataxdu5ayKlBxXHfYBhCFT2rVBtAAgkqFVmTTbgaoNhwkSaS8DcG3ijxrK7yFxKiVVGgUPlZLLirvAgoXd69AIERWX7PZZVmKHdbUtSONG46O4JvYxQg9t2nAGrVeYoSU0+t1sdYzHA5XxEIhYL1rN7XgW6o4QRs0DT4h/n+1sygt0w9xDhLtf683hrVVJPooqRri8MR7T5akLCwttok/k5OT7Nqxk3e+851tm3ciG4CKOMdwMPgz7/3/Fq/CJsH5JA7SWhiC3/GMZ7B46BAP5bWSDBSiJdiJSwJWlbSONO8x1Z9h5/57yZLsBHc+afmAZ4YQboqOYF3g753335VmKYPhkM995hqe+7xnxAw5y7rpqWdY5244sLi0JU9Tksghr70nNQJoNchwrJWlL40LU8ebrdGTV2WBUoo8z5mamWFyeoZzzj+f4WCZ3bv3ML93D/Pz+zhw4ACj0ZCiKGIlYKN/vcU7S+0kq75hpTlnV9Ds+CLPz89z7733Mn9gPpbyYn2tYiZe66QcTz+thFabxBs1yzJB1dMkkqKUzP0j/VZrQ5IYysrFDSrHZWK60TIqY8Ju87w8AecaWa98no6Mv0bEs7w8EIqu8hLYaWL2nnfR0qvJgRD+rSOgjYqAfqCq6kjTlSOydjWZTkh0IilSWolktyxIs1SmCbF1aOb/OqYIVZUVFyBb4CNY6f1KSrCJ7ZovS6qyAh9ItLQHRks24N+/4x3Mz8+fdOmv4G11Xf+crevDWsKTmR4AnHn22Wzdti36Kzx0fg/J4TYAD+4HiXljwczkGibHpkRmaU7KQvmrPoQfIvh3RILQswP+v9rAX05M9rn11jv48pdu5Due8WR27d4Dzh1aPzv9pDRLv7ywNJyWRFxBhVHgY75cQwqR0MgoQGlKxHhDrBgjB6ytqKoCZQxKdemNj7O122Pb9tMJPrC8tMTCwgGWFg9xYP4Ai4uLDAcCGlZlSV3LpuC8GFC6SFSJLTIhBG6//Xb+5DV/uhJyEUdcDcaQZRl5p8v4uPTz4xMTTE5MMjE1zdhYn24wpN6Q+IBJZMGKyY4HbMumEx5CxsGlEQcWlkGvWIK3JpuK1mI7hLhpEkhCiCevWvFTCIEQ6nYc6X0Ab2VRqoC3jhArBTnVavBQllU04Sxb/r6PAqCAULkbWqFJDNb6Nt1XK0Mdx4xGS7tS1RYXHElISLMc5wS/aLgL3iu8tVRVyWS/y3i/y/zBZZSSMefMzAwf+fd/52Mf/egJC34a0VRd1//U7XR/NI041IPp15uf/7KX/Ri9sTH2n0Lp7/GBgDw4Nan3lqnxKWYm13DHjlvpd8dP9lu904dwbgjht+Jb8qbgw8dscLeNj/f4z/+8lrVzs5z/qHPYv38ehbl77eT4kwbD0Re1Nt1u3mVpeRCDIVV7ejQZeNa5GKNlos+Al7w3sa2jkZ81oNWgKNBlhTEq2kQnrFm3jsnZWQmxUBpbVRTFkLIsGQ6WWV5epiwKvLNtr6wVLA+iWaYPOO+EMx8Xe5rIjdztdsk7XTrdHr1ej/HJCYxOyPKcNElaAw0dcRvvrbjrhCDCnEj6UdGKO+tk1LUYeDaMOh9PwERrvFHRtMOjlYmFUhUddTVBB4yBPO8wGAzlJAZJ3TXiyCROuzG8U24G8XKM6V82ZgBgND6SmbSK1Q1NFkH8OApbVSiTYONrRRBVYLeXE7xjeTAShiSpcAhitVI7K0pDJ89P/IYET3DWkSYa6x39/ji7dtzH29/+NrTW5Hl+Qqe/c45+v/+epcWl729ejwd7gDrnOPucc3nad3wHO3fseEgXf9wAjuhLG6HEye9hWO+Z6E/jnKesH5RzyW9D2AbhpZEm/O8hhLPSNKUaDPmXf3o/WmnOO/8s9u8/QKLMTds2zF111659n9l/8FAyPtYTZRrRvzCWhK5xpo2U2CQyyeS1aNR0OkZUyTxZh4COPXcIUNUVYWlRFl9kqhmjGeuPMzE1hXVr6PW6qACDwUiWWwhRuhqi7l1Ket9YV8W0Jen3hb/vncW7EJWWTQchJB4FqMhcNBpMJGQ1SjyCbsVZKkpwe52UJEmx1klPniSUVU1VV4x1cqkMInjpogGLrFKx8x4Mh3HEaMCHSNzxuLixtPTXLMW7OP92tFx8FVjxCSRQuYABQrQFFwBE9P8qbiJNuybVhhcz0vhaJiahiu64tnYU5YjpyUmKshIPPSNVizaa/QcX2zYmz3JM8Pz5G9/Ivn37Trj0N9owHAzePTs7+8Iqr0Tj8CCv5ue/5KUvQ8VAkod+Azhi8Z80ALB6hFGN2LR2C/fO3MWu+Z2MdcYezDd8mfdhI4RnhBDOBP4uhPCjY/0eg+Uh737Xe/m+F34Pjzr/LA7MHyTNki9s27juqvv27P/EsChNN0tFAkrAROOH0IykIiintCaNyHBjzJFm6UraTENWibu0RskosfGoF3tnbJOBGEUwUb5DWRatGMfF6kJQfdOqwkK82a2rUcpQK/B1QbG8gNKJ9PRipNeOH7VW6GhvLX2/aTEAHQM3dCLW2SYabiSpacu8pnzfuXceVSvWzExGCa/DxiCNyjsGVYW3NalXhOjXlyVGvPlCiKCkbvEByeeKgbBNclRE9U2SUFnZAPGeLO/IeM46URh6j2miA7WhriUJCKWp6ypiKTV5lol/ny3bOPEQAolJWR6O2vvYNXoBKws/TQy1D0xOTvKWv/kbrr3mGqamp0+49K/q6m/rqn5p026ob8HTv6nAVs2SVTtTfjCPEGT+e/qmMyMx50Ffz/He3xj7zx8JIfxiXVvyTo73lre/7R+46aavsXZuDdZ6jDafOW393NN7ncyWtY1ccN2e5ir2ugKyqTZWKxBWNO1xTi3elaq1iA4RqQ4x2ltHiatOkih3FQKOMRpby8nakNxUABdpwLaqGY2G1LaWaUJdRfDQYW2Fsyv+97RjdHU/voBi9XRBtePHBt5QDbgaF3z7Oc0EQMmiC+pwD4GVH7PyuSJIknq+qu1Kgm1DzXWu7d/rOirwjCFNE0yaCnuwrgheMJnEJNEezLVxW97a2IKJEUgTleWsvG79sT5Tk5PCFYgjRh0xiBBkkw+N4WdkNCqtxe/Pe8ras3bNHJ/7zKd55zv+nl6vd0zbuKOe/DKqfN1wNHpps6mfiutop//DcemVJKDAgy3+V+9mw2rIupkNbJrbTFEOH+y3rEPgSSGEO6KN1J9AeJb3nl5/DGMUb3/7u7jhhq8yN7dGNONaf+L0jesvHx/rLpQxadWuotdKSEVoF4+P5bCUpvHftBBNGsKOOBNLUGRTGSgkZy+0ElaJKvMx3ipEcooKTbafoOLKyNfhwqpMFr+SAWg02qTopIM2KSbNCDoVoZWSoEwXFF5pnAfroHaesraU1lNaz7CoKEpLWTuGZcHicMTiYMjysGB5OGJpMGJxeZnKupjuUzAsSor4qKoaX9co78ligKf3TngVEffwNPN9Ee00m0KITjzeOuratmw963w78glarK5UtAsjTmTEXcjG+1JRVnUcFYobkPMSF5dEvkVtRTbdVARSRZhY1XnKSioHozXr163l7jtv53WvfS1Ka3r9/nEtfqUURitGRfFbRVH8DzmdT531mvf+YT/9ZQqwwsRrSROnwlOucVmdHp/hjntuIdUPLlBRoRYC4UofwucD4TRCeA9wfvDu9vHxPktLA97+9ncBcO5557C0uESWJtdsnlvzxF3zB69eWB5uzBPJoJO5cWhn8z6O2JSODq2hoQ0LPtDrdBiMiti3x43SrDjR1NGQIs2yePNKkk1z4+nGjy5GV9vaRRGPBt30xj4m0bh2Qw5aYzrjaC1gVlBiVhm0av35faTQtk8cUMq2lY1WJXmeMSpGrZw4bn9xocptnCSG3QcOSFxWJPLIojESqa0UlRdBT2IStErjtECeq49c9yZeK00MlbWRl+CiHDeGuaAlFyC4lnoqCz4hib4GiZZWxqNQzuG9VCCj4RDnhU8gBiHiIBxknstYngmBqaxiG6EITlqBqalJFg4e5DWvfhUHDx6Uvv84NRcSKe5/uqqqN1nnyNMUd4oW4Tej91+pAO4H/T/4FqApP0fliPWzG5kcn6Kuywe5AUAIYVcgXBF8uD2EkIXgPhFCmPI+MD4u3O13vfs9lGXJ1NRkTIbl5k1rZ56wfmbqZhsdbARwUxJLHXPjxHI6kKVpvMkFCbfWshxR706ekyRpy0NvaPtNCVnH/DqdmDYMWdoCFTMNhG+gjZBTalu3MdbWe2xEwZXWVFUtn6uEFVdZOQUbfr1Q7YVzn2qNRmSujd+erAfdSrQTk4hyzxi0Cq1EVshCEYjUWv4/YomllJI7RIm5h4kgZe1c29OHKB+uak/txE7LJImc6BH1bzfaxi7cexy+zedzXn4nZ+tody0kJNEoFLHF0MLmjCavzWaXxvfDOyepwyaNUw7ZvBOTEpxnbKyHs5Y/edWrueWWW5lds+b4QL8AJkmWQ+CZVVW/qSGanarrm3n6xxZgpe9viQin6PerbcXU5DRbN25/sNOA1dd9EC73wX8thLAJwn8450xRlnQ6GQcPzvO3f/v3oGByYkJYec7fOzc9cdnc9NQnCFDWVeTzq9jzqzZa2tpabnS9ihsR26RGCLTC+16ZoDS9N6tEKU2ZqGLb4J1flUuwsggb+nJjbCnAuyJJBPhTQJImUZCkhD8fTy4bBTvNz3fOxx5eRdBN2pMkEbqw80506UpYcrAy6tTxIJBYbRGxaOQGTZMMY1TMahQmpYvGLNrolpgTIuDnvG+fF3GqYOLEQ1jCgjtYZwlOPANQMvojCIfAxRatyQcMUbabJIYkzSKNd2WMnWcGW43wXhSkRVFQ1hVjE306WcYbXvc6vvjFLzA1M3MCDL9wi9H60iQxH34oFuA3q/c/SgVw6gNmFFBWJds2bGd8bLKlOJ6Cax9weQjhBu/DRcD7m5J3YmKCL37xGt7whr8kSQyTkxPUzlFZt7RhZuqqzXOzf9eOuFbFSstkYEVHUFV1G0zanPJFJPcc9uZF5Ltxw0WxsqDjqdzQXEP00tPakEa2YhNymucdVnjTIVqb2dalpq3L4kzcR1BOqdBscjKXT0y7EXnvqWobE3FW1HUSztGMFFfUeb7pzeO4U+K6pb+v6jJOB5wAdd5Fdx55HZ13smnpaM8m4xU57SM/oNkUQ4wKb16vxpjWxzFgM1UhVhe1rVe9BxXD0ZBRMWJFtyIbxaisSFNDajRZmpJlKWmaMjk+wVv+399w9dUfYWJiotWMfMPF78OH0VxC8Dc3k6NTuj6+yaf/ygZw2ChwlYztFDxqWzM+Mc22TWdQVsUpfPFYCPAEH8KnQwjPBN6JEgOLDRvW88UvXstr/+yNGGNYMzOD94GyrpmeGH/J1vVz/zvPcglc8Z4sTeR0j2VjHauApmRvHHhsDLtsY6flJiHExdaMBJWWtkJaAPGgb9Boo/QKJrAKL3HNoozMPWM0nehgLH77wuqzcdMxWlPXlsRokkYcFMFE8RwU3kGiNSpONcSjL3LNo022CpBEDgNN/LYPcQOJoF5kDaq46QgFV75PI7JKkoSqKqWtia9jliTR5iuszpaPhCXJBxTxk4m/H9FnwLVuRiHIqU8QOrFznsSI85FztmVMeh/I0oylYcmhpSFaK7rdHhP9Pn/5F2/kXf/4j/THx8mO4Qp92KIQj4BXlWX5TIVafKgW3zf79G+nAO0e0P7Pqd3qyqpg28btTIxPncoqAAVDCE/y3r87BP8i4G1BSYm5ceN6vvCFa/izP/tzhsMhMzNTABRVTa+T/fbpG+deuGFu7aiZdx9mu9XoA2RHiWsyoJRhNfrbLOTmBFp5U0NrPgkrmXamoclGQ1MfQmtaUscFLv4FzfdybaiGWoXWtj2+jqdmG50Vx15KRR0+saePPnuRqKO1ae21PF56cB8TfH0MzGhca8NKIKgxSYzGlp/X74/FDSVWRfHn60jyCatGZ4lJqCvbjlcjjNJOBJQybVycakHkjG63Kw7DPqC8DEFCcO3vYa2N799KCwaBJM3I85y3/M1f8+53vYve2BidTueYfX8rOoLlEMILgw+/jDrlh/4j6vRveQCqrf9XzY9P4UPMOafYuukMilNYBay6XhhCeF0I4YeBf2gkuBs3buC667/C//xfv8KXv3wdp23ZTJomERwM754ayy9ZNzN53aishKMeWXQm5t4La0wfppNw1ranvnNOHINiOb/iB9DkLYdVG4REnAvtVdBwkQ3LXF0ionX0pBcFX5Oz4JyP1ma+DdRwzkWlX0N2EX6CjTFYnSwDrRkUJUVZrvAfIP6cKBwSAUGbzGsiwakZDDentnc2gnQ1tq5JjKGbd4V7H629Go6A0praurYqstZirST8qnZVhVX5kA7vLLa25J0OY70uwTtRWJYVdS3hH42LkACiYlTStCDeO4qyIvhAb6xPJ8/56ze9iXe/612M9fv0er0HBP3i4vsPZ+3Fwft3q4c4cPWRcPqvVABqFQfgIfi9Gyzg9E3b40Sgeih+l/8RQvj5EMIPAB+WTUAxPTPFcDjkT1/7ej589UdZMztDv9+nqmuKsr5p3cz047dtWPtmYwxlc5LFBdnp5GItRmMnvRKMahq6WiyVG/urw3rIOE7UzcJQK9OWhjQTd+CoTZDevq5XnJV9NNoglsNtKIbSK9HXRk50FYgAnYzltIp05wDWiZOOjwsunv3xxhOKbXMTNr78wYe2wvDEGPjGnstW7Nm/D61WSn1r65gTYFtOgG69DKWlUijSRHpzrRQ2OgA1hdVoOGI0krg3j8zwk0Ti4Fzr87BCM258GGSK4JienSZNE/7sT1/DP/3Tu+mPj9Ptdh9w8TvnyPP8d/r9/lOttbc81Iv/kXL6rwIB1eFxVKe6BFCKuq6Y6AsW8BBVAQCvDcE/24fwDOCGEIImwOzsLFma8kd//Bp+/w9eRbfbZcvmzXjrqa11U/2xn9y6fu7F/W6+XNa2BQWrSkxNXDR3EDNOvYoeLIaX1tqY3hJaVB+k5G6y8lY2hRBHayr+W3S0iX79jRjR3+90CrKIjKGbd1qKnzEyokMRvfflY1UtJ3Oep9Kj60TkzkrIOU740bJwvVQ61rvotS89e5PO62Kct+AEDcYAWZIKMcg55CkoCe2IwpjGEMNoE7+3x3nbTjC8d5FBGPkEWjYi771sJk6mF846Gl+9xiY8SUzbHvkIKq6ZW0NVlLzmj/+YD37wg0xMTj5g2R9v9Vudc09zzv3Ww7UIHymnf9wA1IohyEO58alAURds23TqsYAjrg8Swvk++PXAAeB0oZCOsWbNLB/7+Mf5lV99JV/68nWcdfaZdPIOw6IgTcw7tsyteeyG2emrPZIRr5TQfPMso9vpUNU2nqy6lfOiIM9y0iQVccwqtNjHjSP40PaYWqvoe6cwaRKxAN9KbJue2YeVtL/QiJd0pNxGdF789hMB5KxtF7CM7QyplsXTfP/gA2mStjLfxqhDESIHIhMmZAQoFay4LMVMvtpF27WovmtehzTRZKmwIJP4nFBgI4qvIj8iz3Ock3l/8zt771pGYJaKq0+TeOzdCoHKh8a7ILSz/7oW7GRu7RwL+/fzm7/2Sj75iU8wOT1FGs1fH4Cs9mbnwmNQ6uMNDvBQX4+k0/8wEHD1KFBF2OrUPqCuSybHpzn9oa0CAG4G1vrgv0AId6DUd/rITNyyZTPXf+UGfvM3f5s3/Pmfo7Ri3dw6WUDW3jY3PfmMrRvW/VK/1/Vi6OGo6hrnnZSwKgJkYSWnzjelaKwQjE7klIwn3OE+gNJeSLZhQEfEXCemvRFcVPq1arxmI/Ae6xyjosBosQSv67rl+Fvn6HY60e/eR9qujZ1KAO/w3q42321Vjz54wTdiuS9cABdxCNHpr2x8HoNqTy5hAYZ2lNi4FZk4w3dRU+Gdo6xq0LodCfq4mNM0pdPpUpRlVGHK5ignpFQDnSwjMSnGGBlJ1pbx8T7rN6znhuuv4xd/4ee58cYbmZmdjXZwx1z8XweeEwI/iWL4cC64R9Lp34KAR+/aH5pHURVs23QGE/2HtApo4Mzv9CH8LN5/CPiVZj6/efNmsiznta/9M/74Va9meXmJdevmMElCUVZ0suw1p61f+5jZif7VId7oLpJ0QnsqE/vlWOZbtzLHX3XjCTq/ItJp4qla1qCRUzO0J1Vo+4AmtNUYGbO5CIIpFSm8q15aE9/JuqpXyDOroHalFSaV+Kuqrtsc4sYNSDaBEOXIStqC6Oijo1WZ94HUiClHFW3XifHalfWMSmEv1k5er6aKSpNUWgkxz4ijVVo8IxDIkpRutyPVSEv6SVp1ppC0rFQPtSWgmJqZZnZ2lvf+8z/z66/8Ffbu2cvs2rUP6NgMvAa4CMIHHu7F9kg7/dsWoGGrrWYDhYfgAaKjnxif4vTND3kV0BQ3rw9wSSD8GPDeBvTpdDqce845fOazn+Ml/+XH+cjVH2Xd2jnGx8cpqwrv/A1zM1PP2LZh7U9O9Mf211ZENSqi3Eli2n64OeEbXAAa3z6zMl5adUOu9ne3kS+fJEk8tVd8+ZtBWlXWDVCFD15ELlaQ+2ZzIvodWiE9xdxE0wKQVWWjBZaIZBo+Q4ibQ8ttiJiwKOxcpEhHzoJSK2O8+POSRE7xRrmoaAxTxIBVEnss4v8bx5LxdPcxz1EpxWAwYP/+eUJoPBK8cAoagpMxYiIS5HvMza1luLzMH/ze7/K6174W6zxr5+ZaH8GjXB8HrgB+CSi/GYvtkXb6xw0gtOSftlLVLV506h8KSlty+pYzHmosYPUmcA2BswlhAFwDPKFxXp1dO8twNOJ//87v8uu/9Vt0u11OP/10UIqirEiMfvP6mamLNq+b/ftOnlJZ257eh6HFSspp5xu0WlZSI7IybVCFbbP9mo83fbS45SayIFunXxXbjpVQDd/My4NIj7VWrVV2M7LUSkcmX2iTn4TYG9H5lg23Av+2llxAluVkeS7Mv0YRiWj/G73BqCyprQUVyNIYnqJCqwZs8IUGv5DNz61yB9atcWZo5vgNhbr1TRR1YF1btIas22Xt2jXccN11/OorfpkPffCD9MfHmZqebsHFI667gZcATwM++81aaI/E0x8aV+CWAXi4IOihWpF1XTE5Ps32zWdy3c3XkKbZw/LLBvihAD8M/CnwryGEPyLAzPQ0Y70e//7BDzI/P89LfuRHeOITn4g2hp27duGd3TnR6/1wN8//8tDy8DcWlgZPH5UVScMWjCV0CHLy5olYjoUm0y7O1WXxqPZGbSKufGOPbQTtd9aSRP28iovar6okvHOkURuwmvgjWgLVjhudE3svrRQ6TSjqmiRuEAK60YqLms2otpYkahysC61e3PmGdxAItT8sA0C+n2ufR6ITtDZUVdmqCn0T500gjXz+qqylTYjGKDqardgokhLwUfCQTp4zOTXFaDjgbW99K+94+9upqorZNWvkdL3/4q+AVwF/AAy+2Qvtm6n4e8AN4Ejxz0M8Am2XYlEVbNt8BnfcdxtVWTxsmwDwdgLvC4T/C/wt8Ife+6+macrWrVv58nXXcc2113LlFU/k+77v+7jyyidBCOzetRuTmk/MjPc/0e92vn9hefDrh5YHF1X1Shmbpim9bpfBcBjlv/cPDPHRQachwjSLo6HaJiYhS1KGRRGjtUVdh/MtEUhFApCKMmKRM5vWFacJ3OzknSjDbXz0V1KLfFTgNc5C0peHNkvP2tgyNEYikW7bWI43LsrGJG3SUsMr9yHgykLEQIiq0iRpBAJrsqxDt5NRjApZ6K4mz3KpTpzDOvFmCCGQKEXe6dLv97nh+ut5y1+/mRtu+Ap5nrN23bo2F/KI6y3A7wG38wi4vlluP8ddARxeL59wLthJleVVXTI5PvWwVwHxOgT8F0J4AYH/BlwbQnh7CKHauGEDVVXxyU9+ik996lNcfvkTedlLX8rTv+Pp3HPPPSwuLpFn2bs3zE6/e7I/9hOHlga/vDQcnVXW4kNfmbJl6TU7vzYajRZRTPCtb4CzIudtuOytgjD+KaYlHl+59mRvwLvaWtkcdDMhCNHdRz6ukhifjsJ6i6sgNbqN1W5aBu89RhmR5aoVC7S2spBdqzEnb+XNiUlJk4Tl4UjamMYUUzUgpSJYj1MuBoQ6kREDg8GQYlRgXWidikLw1E6sxJqNcmysx9TkFLt37eKdb/s7/vVf/omqqpmamRY/gPsv/ncArwa+xCPoanr9H/vxn3hEnf6HbwCrC4GHihJ4OC2Aoio4fcs3pQpo0sP+JcD7IbyEwMuBT3nvr9Fas3nzZqyt+fjHP8b111/Hj/zIj/DSl76Ubdtmufvuu2XWnpo3r5uZ/JvJ/thPLSwv//yh5cHZBw4t0+1kLZ9dytOVYEgpjeu2/A+N4lALeDUqylZ7n6by9tjGC7/h+AcnCxFF0CpCOMLnz9Kcqi4j9iA9f5N0bOuKJFGtvj7EckA2KdOGWTR8cGEySuvinWsrGpl4KLRecVbOskxK93jD+1XR5F6C+0gi3iEeoVrK+1iBVlUlI9I0IU0SJiYmUQre/9738K5/+AfuvvtuOp0Oa+emW67Dquttsa17RC38ptXr9Xp87/e/kMdefPEj6vSXDUCtCma9Hxbw0K7ARiOwfcuZXHfTw14FrO4V3wycFlHiOeCL3vt9Wmu2b9/OYDDgda97Hddccw0//dM/w+WXX86unTspqhKvtE+N/vN1M9N/MT0+9hOHlob/Y3E0elQV5/OmkbyGZqIvp2gTay6WYXV0742ZAFHaLDbazWhQtTl3K1OagI7jO1EbGmpbMzk+DlqxuDSQntpJjp8ykRYc7bWSKA4Siy1IlEaZaJ0WAcLgPSHalzUsQW2EmjuoSlnUjbIw9vuJEVORllUoO59MF4yWSUVVRA0FMblHkXcyJiYmSE3CDV+5nnf+/d9zzRe/AMDM7CwqPod4DeLCfz1w4yOl1F+98MfGxvie57+A5z7v+Zx55pns2LHjEXX6CwYQS361qgR4eHAAuYvLquSM087i5ltu4uDBA0xNTa9CqB/W6574OBc4H/EcuMM5VzSTgc9//vN8+ctf5g9+/w949rOfzcFDCywuHCIIXTikafJX62en/2rK9n9gaTD8icGofMZgVBAQiyy1Cl511hHigleoVvijW7svOXWb07oZ2TUyWdkwQmT6iUW2jnLhqrZxTBlttkOQ8Iwo9FFBSESNFZr08TV5NyM1BlcUrGSmqEhoahyVhQHYMAp9TDsS/gHthmetQ2vIkw7eO2pnKb1FOU2eZoSkqVoC3V6XsbE+Wim+cv31fOB9/8anPvEJnHNMTE6S5Xl0/PEAtwF/E/v8XY+Uha+UYhQt0w9b+GedxcEDB7jrrrtaXsgj6VJ/9vtvWUStyg0Xc4YlYBOw9HCgo51Ol0G9wA1fvZ4777iDEGB8fPywjUChcMHhgltl5OHbsZIPzcdWVGi+ZcOt+v8xTbb5et9k2fkmyio0X78NQua9XwTmgdoYw6FDhzh06BDPftaz+f0/+AO0Utxzzz2kWdYad0ip67HOXzYqqv+yOBj+QFHX0wFwtSNN5bRvMuPbEVkzS1dK8gT1SppZM0prRo3NGC0EDvs+AUVVryzGgERlhSBZeiamDzVgpHVWQjWVpPY0GMTKYm+yAoNkFEZAS8VAEeuE7ehDg1uEVmCkjZYocSvAXpYl2Dj711GFODU9DSFw3Ze+xIf+/QN85lOfoq5remNjjI2Nte8l8H7g74B/PDUglbRMsllZxno98jxn//79dDsd6uh63OlIEEttHaPRCB/VjWmSUFc1W07bItVgUTA5Ocmzvus5PO8F39su/EMx2089bKfqUa9xYEf8c/W1dL9cgIcJB2yBKwLormPb1m1sPX0rd911BzfeeAN33nkXIYR2I+BhLwi4KzJY1wAzwNA5N5qYmLCdTod/e9+/sXvPbn7t136NbVu3sXffvlYh6OLGhFL/Od7r/udYJ//VytkXLg/LFw1Go6fVtcVGRL9ZxI3YqAHfkkiiaefqIcgmpVxE8aVUz5IE5+P4MPbF2f/X3pU2R3Wd6efcvVdJGDACCwGpgINZLQGS4xi8ZGKTOLZTiSeTzK/Jl/k6mV8wH6cq82FqpmoqVUlNjTPjjLHBwpKQjQQSzmgF9Sb17b7bOScfzrmbusVmFiHft4pSI/WiVt/n3c77Po+mpZaSwMO0HoAScwUQiIEf1/fkmX1IXy6crUIUsRkIMUMQrjiH7EKB3DrklMgjTxKdaETZiOcJjkFVidiWrHwepVIJzbV1XPr4Y/zh97/HJ5c+AQ0C5PJ59Pb1CedO6VcAfgfgXwBMbanIScRx5cL8PHbv3o3zr7+Bd9//AAcPHkK9VsWtubmtAPz7yAD+4Z/XAJRIogNICB57BsAk+PM7LBg5BdQXkaZULoFzjlu35oQjkBlBuVQGJxwBC55UBhDpBUis5OVXmxDCCSGYm5vD6OgofvuPv0XbccSRm5yqpJL0g3ABCtPSwSkH5fyY53k/a7adD9qud8rzA/iB2DPQ5ZJOWOqH665RBqAq8r0KEFqGKXbjQxVeGa0ld30UxcPhnWicWR5RhtoGHExSnEseA3keH60nShYiyBFjIWAqeQM4kXJiJMpSwgnksFGnKipy+RysnAVNUbG0uIjPLn2C//3TnzAxPi4aZXHEv8U5/w+Iqc0/Ps7680EzAMdxQDmDY4tUv1zuwS9/9Sv86J2L6N+7F2uNBtbX1zuHxJ6+3SUD2NjyewK/t9gxB/J9JvScChrEx0+NRgOKouDgoUM4cOgQbs3OYXJiHLfmbiFgAcycGTHtPEHjiIdJSOgc9uzZg6+//hrz8/PYu28fgsC/S0eYifepKJP5nDVZyFu/CQJ6uu3677Yd913H94cdz5O1s+jw67oaNd0Edx+LGn6qosJxHVDOYaiaVBSQHH8BRRBQ0fGXqbgYNfaiaUGaGBoinETEBCxxDBceB6pEiZuRkuZbDBjJkWUej5BTFoqpqDBzJgr5AgzTQKNWw9inn+HTTy/h8qVLWF5eFldmTxmWad1kjP0npfTfIUZ26VZCTyh6YjebAIBiqYyfvvc+3n3/PQzuH0S9XsfC/HwE/K0e9dNNwGiii+BxUiCl0n4Jfs3SwALWkVptdAQHv3MQt2bncHXscyytLKHRaMA09ad1asCTxzzlchmGYdy3rlyyu885xixTH8tb5m8oo4c9L3jb9f1X2657wQ/YLj/wxQKQ3A5UJftuRPWt6VAoR0CDKFoHfgBDN1AqFKBqGmzbBqMUXigcSmKVH5KI0iGFmfi5IC9VIzYiJoeJFDDBOgIqxTeIHDCC1FAwLRP5vEiW7PU1XJucwNUrl/HF1auYvn5dsChpmt23Y8fHqqp+xBj7I6X0MtIUCFumo++6DqqrFXAAZ0dG8fLQMEZfeQXfO3oU1UoF8/PzqUbgs2ZazFSzcQ7g8YCfEyDfY0K3NBn5N/8AQkegqgQHDx3EwP4B1Gp13LgxjfHxL3D79gpMyxR77M+gcckXQMFACJ/O5czpfM78pzLN5xjnZ13P+35A6ZmW456ljO+lnMFzfZBwZNjzJW+9nAKUZ/JcsgqTkLmXA2A0ZH8QVN/h4hJBVHIEjMpoH/MLCmehRR170SNSBJW3Jth98oUiTFOH53po1GqYuHoV1yYnMTU5gRs3ZuC53gqAz4ul0mXLsj7lnP+Zc159EDHOJ1vbB1Hzrqe3B2dHRvDzD3+JsyMjKBaKWFlZfmZq/Hs7ANJ5Pv84NgFE2k+Q6zGgWWpH5L/bBxI7AhU9vb34wQ9ew9GjL2FiYhxfjI9hZXkZhmnCMPSoo/7sOQNE0RYgbQAf5UzjI1VVUS7mDYUoR+xWe5hxfswPgmMuZUc9z9tLA6ZwxkARR3geUDhOU27fifSfKEq01BNqBtBQvoyKOfxw8YaARGrJqqpEz2sYYj9BU1UUSyUQhaC+WsH0V1NYWVzElSuX6cz09OLC/Px1xtgkgC8LxeLlUqk8BcAJ+zBbEfSUUjSbTdjNJlRVxb6BAZw/fwE/eucd7N27D7lcDrdv38by0lK04r0dTOs2AfgoG+6h7jsHkI/Azx941iDcpnKcNlynjVwuj9dffxPHjp/AF1fHMPXlJO7cWUG73Ua5XN5y560PmhkAkNx+HAA8RVMmNFWdKOQteL4Pxolu6Pr+tts+TCn7DgcO+gEd8Cntp5Tto5Q+F1BW5pxF9TmJjgolGScXmoKKpCIX/AMEUDh0VZc6iIosVxgC3/cJQdVteSuXL/3f0tjlKytzc7NfLy4s/MW27VsQs/fzpmX5pVJJNlHxNGY67jOwMLiuC6fVgqPrOPrSSzh/4XWcHhrCqdMvY0dfH2y7hWaziWq1GvM7biPTklTY4sPvKHW/EfzFOipHvmRAMxWw4NFcDK7rwHXbKBQKeOPNtzAyOoqFxXn8+eP/wdTUJBzHQaFQFCw+T2ew6JE2Hbh0Bn5A4QcUIIpPgJsAuWlJyjLf8+EGPihjat4yS5zzfj+gOwJGSzRgvQFjveDMDBjVGYPOGFPFwQEBURQe+D6zdC1gjHqe53qMMbtaq9Ycp71WWa2s/fd//WHl3Cuv3tl/YND9t3/9Ha5fnxb9nFxONPRK5eg0YiMHwlYBved58H0fTrstU/xenDh+Aj/7xYc4evQontu5E77vY63RwPLycpwxbTPgx03A1F8o5Lt5NASB4QWQL5vQDQWUPuoxYwLHceA4DnRdw5HDL+Lwd1/E+PgYZm/ewOzcLJaWFlBv1FEqlQUTraokGOufbQs79ZQx+EEAnwpacMoY5Rx1Qkg9HEcmavgYQE2oQAu9ACEPxsK5BCaoxSmlcB0H7VYLdnMdtWoVnu+jUChi565duDU3B88PYJgmWhJQWx30hmGgb8cOnDx1CmfPjeDkqdMYHDyAXC6HSmUVS4uLKZKX7ZLq37sHsGEg6BthhMTdfqusQw3B/xj/lkEQoFqtgigKjh8/haGhM6g3GpidvYHr17/EzZszWFwUzqBc6pH9AiXm9tpmJkUuBE9A4l8cnZH4imhXgUdEHJLCTFWhakK118rlojXdrWphqei6DmhA4bouLMtCT28vjh07jr/79d9j/+Ag8oUCdu/ajdXVO2g211GtVqIov91Bv6EEQBdmm28WqMOy08zrUHQVnPIn9uGLhmFdinkaOH78JIaGzqBRr2Hm5gy+nLqGmZlpLC7Oo16vodzTA9M0oCoPf2F/ey6XZwPwAPDczl0olooYHj6DoeFhfPfwERSKRZSKJdTrNfieh9nZmxHot2uKf/8ZQFhtchJpxj8s+AEOo2BAMZRIKfdpmO/7aNRqgEJg6DpOnjyNM2fOoV6rYWbmOq5dm8D16a+wtDSP1TsVlMolFHL5SGBzM4cQOkzbtuG5LhgXlFiuK5poJPMIjz+l9zxwAK7jpAB/bmQUfb29ODcyimKpiBde2A/Xc9FcX4fnulhcW/vWg35DDyCkoE2E/YcJhCQB/rwuZKkp3wpXTOQMavUqCAgM3cDp08M4NzKKarWCqalrWFiYx/z8X3BtcgK3V24jkEM+CgFMy0plSc3mOhqNBgYG9uPNt/4GASe4U62L3XtVA1GFgAeT8/sRTVdmDxXdnXYbIATtdgvgQC6Xw3M7d0JRFIx+/9UU4PftG4CiKKjVa6ABxeLiQqqmz0DfpQRIS1o9uA+QU20A4dAl+EOGmy11QSF2BtVqBUQRZcKZMyN4660yKtUKFhcXMDk+jsZaHWOfX4ZtN7G0tAjOOYrFIprr63hhYD/e/+DneOPNH+KFgQEsL6+gWauLJRwQGIYWLdFYUpyCQI0IMcJmK4n+cBnQI6DLvkUY2QFgT38/At/H0PAwDh95EYODB3Ds+HE4joMDBw6mAL+0lG7ifdtq+ocuAUiqCXD/JQCBpKMCoOdi8G/1ylhcFBy+76Fer6Fer0DVdOzduxdHjnwPYAzLF38Cu9XC2JXPUKlWcHXsCi7+5D28/fZF7N8/iEajgcWlBYCLEiPc2mu7nuTmI3BUD5wDpqdJ0k+CnCl08WhiKAdJCi7ENOPbEeThfkOrZacizZ7+fnAOGIaO185fgKZpUDVN3lbR17cDe/r7Ua/V0WrZMC0rA/wjaQJGSH6Iml8ul+iWlgD/s3iBCrYZ27bRbNpQAFhWDoVCCR/+7a8R0ADLy0t4fs8eOO02FhbkDLgSL8KEEuNqSglIbO45nhft4LueHzH9GpoGTSr5mIYBVUEkyKkq8lgOQrBTSQiMIHn7KVtIhNFqtcB4vMdj282Oa2pPfz80uaPw8tAQTpw8hXqtBiuXw2vnL8AwdHAODA4OQtcFzVij0YjEVG7MzHSk8Rngv3EPAKk04L7/nnLtU7dEdGPbJGIRORnnug4AAttuQlEVFAoFVCqrET/f/T4XgWTwlWUIkwo/AIHr+3Dk/qwXBFFmwhhg6hpUyQTkSCVdXQ91AbhchxYMupqqxoy9XOzfh8ex4TEfYaEIzF2cORCvTlMK3/Pgug5adhO3V5Zh200EQYDK6qpwZBDDNLZtI58vSJkw8RynX34ZJ06egt1swvf9COSWZaHdbmP37t3Y/fzzkVpQo9GIrqHkEE7Sshr+kWcAvEvafx8Xd6gOY6lQVJISxdyONSrnHI7jRMIb37z8SPQlEspCUdpPCBzPj4aWQoJOf51GNEHNtkirfUOHF1BJRCJ2+9fstsgyWHz+H0l9S8cd/x5ih19VVTn+q8GycuCcYdfze9Ab+OjbsQtmroDBg4fQ09uHH//0PZw49f9QFAVvv3MRpmWllG6CIIgA7nleJFfWaDTAGEO5XIbneZi9eXNTUGeR/Un1ADbuA/B7twE5BzRTEeBnyA7DH1lfIvF/pXMgI1qmkZ8RIQSO66PluDLbEB9m2/WjbC7aBEiTvkRfQ65BRQn5BzSYmtgFKJTKQvBU0zB8bhR2swnXdfDjd9+Ta79AtVJJ9CvicuheAM8i+pZwABERVFwCKBB7Ad2zfvFAQ4GikS22xf3tcxICRAQK1PT31Af3yMkSIKY0lzW9C7TsZnTfyuoqKvfxnBnAn4EmICTeSSIdvFvdrxoKiIz8WeDPLLNn2AGEqq9h3iZZ4TpzeukUVIOIxRK+9Y/6Mssss3s4AB4RgPDN00yJdUUTZJDIZlcyy2y7lADyBICTRFDf4ACIqCkjnvos8GeW2TYqASTIE4dCCScgBlnIE9IKyCyzzJ5kBtDBByC2AcOzZkIUgHTtCmSWWWbbJgNIZwKcg6wTkqX7mWW2DWx9s/xdi6K/1KKTpigK+gE0s9CfWWbPvBWRYPvscABh3y/RAywAmO7mNcg9v4God9CZPpDUQ8g975/+ccfPu9y9o4eZWG+O2xg8le4Qfo/n6PqSJC2lHt7s/pZD/eXMmWb2NIwAKGzaAwixRWIQEhAUeWJkFGFbQEk0B5NI5smtQpIgGA3vQzYFV4cDir5PYtSGGnkR0Lo/X3qfoTN9Id1uk4345TFgOzwW6eolNpdXIzEJaQb+zLZcD4DEkVBALg6JyYCWDMBifpxsOBMkqW1CHp4lbBIR49dLxePu+CLxzPq9MpC0zFmscUBIZ6Mj+YyE3CMjIZ0pCen6Hgk2xPwM/JltXQeQjmLdQjoSANkQ8ZQQwKkzxC7dhhhkIovogsUO8McRPHI83fL1NOY6mc1IFwfWDZUc4GGWQTrBmzoFJen3lHI8Uap0X3VSZpk9bQeAUpwAJGIsIXdJnrtgcLPynKfbD0nwkw0ROAWibq9J0jnAxgDNSecvEgOXdG8nkMRTb/K66W26hFPiuEsTIkv7M9vyVtJAsESA4qYdKrJZ+r6xqUbSfTGyWXqfjJy8+4slsgJOkmjnyT5Fx30JIV0SmOQabLe0PvGYlAPcDL+kaxNBODMe387OTzPb+tb8K5WZ5GwZzMJTAAAAAElFTkSuQmCC"; - private const string _lightlessLogoLoading = "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"; - private const string _lightlessLogoNsfw = "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"; - private const string _lightlessSupporter = "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"; - private const string _noUserDescription = "-- User has no description set --"; - private const string _noGroupDescription = "-- Syncshell has no description set --"; - private const string _nsfwDescription = "Profile not displayed - NSFW"; - private const string _loadingData = "Loading Profile Data from server..."; - private readonly ApiController _apiController; - private readonly ILogger _logger; - private readonly LightlessConfigService _lightlessConfigService; - private readonly ConcurrentDictionary _lightlessUserProfiles = new(UserDataComparer.Instance); - private readonly ConcurrentDictionary _lightlessGroupProfiles = new(GroupDataComparer.Instance); - - private readonly LightlessUserProfileData _defaultProfileUserData = new(IsFlagged: false, IsNSFW: false, _lightlessLogo, string.Empty, _noUserDescription); - private readonly LightlessUserProfileData _loadingProfileUserData = new(IsFlagged: false, IsNSFW: false, _lightlessLogoLoading, string.Empty, _loadingData); - private readonly LightlessGroupProfileData _loadingProfileGroupData = new(_lightlessLogoLoading, _loadingData, [], IsNsfw: false, IsDisabled: false); - private readonly LightlessGroupProfileData _defaultProfileGroupData = new(_lightlessLogo, _noGroupDescription, [], IsNsfw: false, IsDisabled: false); - private readonly LightlessUserProfileData _nsfwProfileUserData = new(IsFlagged: false, IsNSFW: true, _lightlessLogoNsfw, string.Empty, _nsfwDescription); - private readonly LightlessGroupProfileData _nsfwProfileGroupData = new(_lightlessLogoNsfw, _nsfwDescription, [], IsNsfw: false, IsDisabled: false); - - public LightlessProfileManager(ILogger logger, LightlessConfigService lightlessConfigService, - LightlessMediator mediator, ApiController apiController) : base(logger, mediator) - { - _logger = logger; - _lightlessConfigService = lightlessConfigService; - _apiController = apiController; - - Mediator.Subscribe(this, (msg) => - { - if (msg.UserData != null) - { - _logger.LogTrace("Received Clear Profile for User profile {data}", msg.UserData.AliasOrUID); - _lightlessUserProfiles.Remove(msg.UserData, out _); - } - else - { - _logger.LogTrace("Received Clear Profile for all User profiles"); - _lightlessUserProfiles.Clear(); - } - }); - - Mediator.Subscribe(this, (msg) => - { - if (msg.GroupData != null) - { - _logger.LogTrace("Received Clear Profile for Group profile {data}", msg.GroupData.AliasOrGID); - _lightlessGroupProfiles.Remove(msg.GroupData, out _); - } - else - { - _logger.LogTrace("Received Clear Profile for all Group profiles"); - _lightlessGroupProfiles.Clear(); - } - - }); - - Mediator.Subscribe(this, (_) => - { - _logger.LogTrace("Received Disconnect, Clearing Profiles"); - _lightlessUserProfiles.Clear(); - _lightlessGroupProfiles.Clear(); - } - ); - } - - /// - /// Fetches User Profile from cache or API - /// - /// User Data of given user - /// LightlessUserProfileData of given user - public LightlessUserProfileData GetLightlessUserProfile(UserData data) - { - if (!_lightlessUserProfiles.TryGetValue(data, out var profile)) - { - _logger.LogTrace("Requesting User profile for {data}", data); - _ = Task.Run(() => GetLightlessProfileFromService(data)); - return (_loadingProfileUserData); - } - - return (profile); - } - - - /// - /// Fetches Group Profile from cache or API - /// - /// Group Data of given group - /// LightlessGroupProfileData of given group - public LightlessGroupProfileData GetLightlessGroupProfile(GroupData data) - { - if (!_lightlessGroupProfiles.TryGetValue(data, out var profile)) - { - _logger.LogTrace("Requesting group profile for {data}", data); - _ = Task.Run(() => GetLightlessProfileFromService(data)); - return (_loadingProfileGroupData); - } - - return (profile); - } - - /// - /// Fetching the user profile data from the API - /// - /// User you want the profile from - /// New entry in the user profiles to fetch - private async Task GetLightlessProfileFromService(UserData data) - { - try - { - _logger.LogTrace("Inputting loading data in _lightlessUserProfiles for User {data}", data.AliasOrUID); - _lightlessUserProfiles[data] = _loadingProfileUserData; - var profile = await _apiController.UserGetProfile(new API.Dto.User.UserDto(data)).ConfigureAwait(false); - - LightlessUserProfileData profileUserData = new(profile.Disabled, profile.IsNSFW ?? false, - string.IsNullOrEmpty(profile.ProfilePictureBase64) ? _lightlessLogo : profile.ProfilePictureBase64, - !string.IsNullOrEmpty(data.Alias) && !string.Equals(data.Alias, data.UID, StringComparison.Ordinal) ? _lightlessSupporter : string.Empty, - string.IsNullOrEmpty(profile.Description) ? _noUserDescription : profile.Description); - - _logger.LogTrace("Replacing data in _lightlessUserProfiles for User {data}", data.AliasOrUID); - if (profileUserData.IsNSFW && !_lightlessConfigService.Current.ProfilesAllowNsfw && !string.Equals(_apiController.UID, data.UID, StringComparison.Ordinal)) - { - _lightlessUserProfiles[data] = _nsfwProfileUserData; - } - else - { - _lightlessUserProfiles[data] = profileUserData; - } - } - catch (Exception ex) - { - // if fails save DefaultProfileData to dict - Logger.LogWarning(ex, "Failed to get Profile from service for user {user}", data); - _lightlessUserProfiles[data] = _defaultProfileUserData; - } - } - - /// - /// Fetching the group profile data from the API - /// - /// Group you want the profile from - /// New entry in the group profiles to fetch - private async Task GetLightlessProfileFromService(GroupData data) - { - try - { - _logger.LogTrace("Inputting loading data in _lightlessGroupProfiles for Group {data}", data.AliasOrGID); - _lightlessGroupProfiles[data] = _loadingProfileGroupData; - var profile = await _apiController.GroupGetProfile(new API.Dto.Group.GroupDto(data)).ConfigureAwait(false); - - LightlessGroupProfileData profileGroupData = new( - Base64ProfilePicture: string.IsNullOrEmpty(profile.PictureBase64) ? _lightlessLogo : profile.PictureBase64, - Description: string.IsNullOrEmpty(profile.Description) ? _noGroupDescription : profile.Description, - Tags: profile.Tags ?? [], - profile.IsNsfw ?? false, - profile.IsDisabled ?? false - ); - - _logger.LogTrace("Replacing data in _lightlessGroupProfiles for Group {data}", data.AliasOrGID); - if (profileGroupData.IsNsfw && !_lightlessConfigService.Current.ProfilesAllowNsfw) - { - _lightlessGroupProfiles[data] = _nsfwProfileGroupData; - } - else - { - _lightlessGroupProfiles[data] = profileGroupData; - } - _lightlessGroupProfiles[data] = profileGroupData; - } - catch (Exception ex) - { - // if fails save DefaultProfileData to dict - Logger.LogWarning(ex, "Failed to get Profile from service for syncshell {group}", data); - _lightlessGroupProfiles[data] = _defaultProfileGroupData; - } - } -} \ No newline at end of file diff --git a/LightlessSync/Services/LightlessUserProfileData.cs b/LightlessSync/Services/LightlessUserProfileData.cs deleted file mode 100644 index 3319043..0000000 --- a/LightlessSync/Services/LightlessUserProfileData.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace LightlessSync.Services; - -public record LightlessUserProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Base64SupporterPicture, string Description) -{ - public Lazy ImageData { get; } = new Lazy(Convert.FromBase64String(Base64ProfilePicture)); - public Lazy SupporterImageData { get; } = new Lazy(string.IsNullOrEmpty(Base64SupporterPicture) ? [] : Convert.FromBase64String(Base64SupporterPicture)); -} diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 79434c2..758b9f5 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -6,9 +6,12 @@ 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,12 +23,15 @@ 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; @@ -65,6 +71,7 @@ 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 DownloadStatus) : MessageBase; public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase; @@ -72,11 +79,18 @@ 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(UserData UserData) : MessageBase; +public record CyclePauseMessage(Pair Pair) : 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; @@ -85,8 +99,11 @@ 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; @@ -107,10 +124,16 @@ 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 \ No newline at end of file diff --git a/LightlessSync/Services/NameplateHandler.cs b/LightlessSync/Services/NameplateHandler.cs deleted file mode 100644 index 11af974..0000000 --- a/LightlessSync/Services/NameplateHandler.cs +++ /dev/null @@ -1,694 +0,0 @@ -using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; -using Dalamud.Game.ClientState.Objects.Enums; -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.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; - -namespace LightlessSync.Services; - -public unsafe class NameplateHandler : IMediatorSubscriber -{ - private readonly ILogger _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 ImmutableHashSet _activeBroadcastingCids = []; - - public NameplateHandler(ILogger 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(this, OnTick); - } - - internal void Uninit() - { - DisableNameplate(); - DestroyNameplateNodes(); - _mediator.Unsubscribe(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) - { - if (args.Addon.Address == nint.Zero) - { - _logger.LogWarning("Nameplate draw detour received a null addon address, skipping update."); - return; - } - - 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 rootNode = nameplateObject.Value.RootComponentNode; - if (rootNode == null || rootNode->Component == null) - continue; - - var pNameplateResNode = nameplateObject.Value.NameContainer; - if (pNameplateResNode == null) - continue; - if (pNameplateResNode->ChildNode == null) - continue; - - 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; - rootNode->Component->UldManager.UpdateDrawNodeList(); - pNewNode->AtkResNode.SetUseDepthBasedPriority(true); - _mTextNodes[i] = pNewNode; - } - } - } - - private void DestroyNameplateNodes() - { - var currentHandle = _gameGui.GetAddonByName("NamePlate", 1); - if (currentHandle.Address == nint.Zero) - { - _logger.LogWarning("Unable to destroy nameplate nodes because the NamePlate addon is not available."); - return; - } - - var pCurrentNameplateAddon = (AddonNamePlate*)currentHandle.Address; - if (_mpNameplateAddon == null) - return; - - if (_mpNameplateAddon != pCurrentNameplateAddon) - { - _logger.LogWarning("Skipping nameplate node destroy due to addon address mismatch (cached {Cached:X}, current {Current:X}).", (IntPtr)_mpNameplateAddon, (IntPtr)pCurrentNameplateAddon); - return; - } - - for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) - { - var pTextNode = _mTextNodes[i]; - var pNameplateNode = GetNameplateComponentNode(i); - if (pTextNode != null && (pNameplateNode == null || pNameplateNode->Component == null)) - { - _logger.LogDebug("Skipping destroy for nameplate {Index} because its component node is unavailable.", i); - continue; - } - - if (pTextNode != null && pNameplateNode != null && pNameplateNode->Component != 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 currentHandle = _gameGui.GetAddonByName("NamePlate"); - if (currentHandle.Address == nint.Zero) - { - _logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh."); - return; - } - - var currentAddon = (AddonNamePlate*)currentHandle.Address; - if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon) - { - if (_mpNameplateAddon != null) - _logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached:X}, current {Current:X}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon); - return; - } - - var framework = Framework.Instance(); - if (framework == null) - { - _logger.LogDebug("Framework instance unavailable during nameplate update, skipping."); - return; - } - - var uiModule = framework->GetUIModule(); - if (uiModule == null) - { - _logger.LogDebug("UI module unavailable during nameplate update, skipping."); - return; - } - - var ui3DModule = uiModule->GetUI3DModule(); - if (ui3DModule == null) - { - _logger.LogDebug("UI3D module unavailable during nameplate update, skipping."); - return; - } - - var vec = ui3DModule->NamePlateObjectInfoPointers; - if (vec.IsEmpty) - return; - - var visibleUserIdsSnapshot = VisibleUserIds; - - var safeCount = System.Math.Min( - ui3DModule->NamePlateObjectInfoCount, - vec.Length - ); - - for (int i = 0; i < safeCount; ++i) - { - var config = _configService.Current; - - 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 pNode = _mTextNodes[nameplateIndex]; - if (pNode == null) - continue; - - var gameObject = objectInfo->GameObject; - if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - // CID gating - var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject); - if (cid == null || !_activeBroadcastingCids.Contains(cid)) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - var local = _clientState.LocalPlayer; - if (!config.LightfinderLabelShowOwn && local != null && - objectInfo->GameObject->GetGameObjectId() == local.GameObjectId) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - var hidePaired = !config.LightfinderLabelShowPaired; - - var goId = (ulong)gameObject->GetGameObjectId(); - if (hidePaired && visibleUserIdsSnapshot.Contains(goId)) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - 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) - { - _logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex); - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - root->Component->UldManager.UpdateDrawNodeList(); - - bool isVisible = - ((marker != null) && marker->AtkResNode.IsVisible()) || - (nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) || - config.LightfinderLabelShowHidden; - - pNode->AtkResNode.ToggleVisibility(isVisible); - if (!isVisible) - continue; - - var labelColor = UIColors.Get("Lightfinder"); - var edgeColor = UIColors.Get("LightfinderEdge"); - - var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f); - var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f; - var effectiveScale = baseScale * scaleMultiplier; - var labelContent = config.LightfinderLabelUseIcon - ? NormalizeIconGlyph(config.LightfinderLabelIconGlyph) - : DefaultLabelText; - - pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; - pNode->AtkResNode.SetScale(effectiveScale, effectiveScale); - var nodeWidth = (int)pNode->AtkResNode.GetWidth(); - if (nodeWidth <= 0) - nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); - var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); - var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f; - var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier); - pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255); - AlignmentType alignment; - - var textScaleY = nameText->AtkResNode.ScaleY; - if (textScaleY <= 0f) - textScaleY = 1f; - - var blockHeight = System.Math.Abs((int)nameplateObject.TextH); - if (blockHeight > 0) - { - _cachedNameplateTextHeights[nameplateIndex] = blockHeight; - } - else - { - blockHeight = _cachedNameplateTextHeights[nameplateIndex]; - } - - if (blockHeight <= 0) - { - blockHeight = GetScaledTextHeight(nameText); - if (blockHeight <= 0) - blockHeight = nodeHeight; - - _cachedNameplateTextHeights[nameplateIndex] = blockHeight; - } - - var containerHeight = (int)nameContainer->Height; - if (containerHeight > 0) - { - _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; - } - else - { - containerHeight = _cachedNameplateContainerHeights[nameplateIndex]; - } - - if (containerHeight <= 0) - { - containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY); - if (containerHeight <= blockHeight) - containerHeight = blockHeight + 1; - - _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; - } - - var blockTop = containerHeight - blockHeight; - if (blockTop < 0) - blockTop = 0; - var verticalPadding = (int)System.Math.Round(4 * effectiveScale); - - var positionY = blockTop - verticalPadding - nodeHeight; - - var textWidth = System.Math.Abs((int)nameplateObject.TextW); - if (textWidth <= 0) - { - textWidth = GetScaledTextWidth(nameText); - if (textWidth <= 0) - textWidth = nodeWidth; - } - - if (textWidth > 0) - { - _cachedNameplateTextWidths[nameplateIndex] = textWidth; - } - - var textOffset = (int)System.Math.Round(nameText->AtkResNode.X); - var hasValidOffset = true; - - if (System.Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0) - { - _cachedNameplateTextOffsets[nameplateIndex] = textOffset; - } - else if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue) - { - textOffset = _cachedNameplateTextOffsets[nameplateIndex]; - } - else - { - hasValidOffset = false; - } - int positionX; - - - if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal))) - labelContent = DefaultLabelText; - - pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; - - pNode->SetText(labelContent); - - if (!config.LightfinderLabelUseIcon) - { - pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize; - pNode->AtkResNode.Width = 0; - nodeWidth = (int)pNode->AtkResNode.GetWidth(); - if (nodeWidth <= 0) - nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); - pNode->AtkResNode.Width = (ushort)nodeWidth; - } - else - { - pNode->TextFlags |= TextFlags.AutoAdjustNodeSize; - pNode->AtkResNode.Width = 0; - nodeWidth = pNode->AtkResNode.GetWidth(); - } - - - if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset) - { - var nameplateWidth = (int)nameContainer->Width; - - int leftPos = nameplateWidth / 8; - int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8); - int centrePos = (nameplateWidth - nodeWidth) / 2; - int staticMargin = 24; - int calcMargin = (int)(nameplateWidth * 0.08f); - - switch (config.LabelAlignment) - { - case LabelAlignment.Left: - positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos; - alignment = AlignmentType.BottomLeft; - break; - case LabelAlignment.Right: - positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin; - alignment = AlignmentType.BottomRight; - break; - default: - positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin; - alignment = AlignmentType.Bottom; - break; - } - } - else - { - positionX = 58 + config.LightfinderLabelOffsetX; - alignment = AlignmentType.Bottom; - } - - positionY += config.LightfinderLabelOffsetY; - - alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8); - pNode->AtkResNode.SetUseDepthBasedPriority(enable: 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(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 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 cids) - { - var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); - if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet)) - return; - - _activeBroadcastingCids = newSet; - _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); - } -} diff --git a/LightlessSync/Services/NameplateService.cs b/LightlessSync/Services/NameplateService.cs index 8ccc362..4ca8a2f 100644 --- a/LightlessSync/Services/NameplateService.cs +++ b/LightlessSync/Services/NameplateService.cs @@ -1,114 +1,254 @@ using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.Gui.NamePlate; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.NativeWrapper; 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; +using LightlessSync.UI.Services; using Microsoft.Extensions.Logging; +using System.Numerics; +using static LightlessSync.UI.DtrEntry; +using LSeStringBuilder = Lumina.Text.SeStringBuilder; namespace LightlessSync.Services; -public class NameplateService : DisposableMediatorSubscriberBase +/// +/// NameplateService is used for coloring our nameplates based on the settings of the user. +/// +public unsafe 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? _nameplateHook = null; + private readonly ILogger _logger; private readonly LightlessConfigService _configService; private readonly IClientState _clientState; - private readonly INamePlateGui _namePlateGui; - private readonly PairManager _pairManager; + private readonly IGameGui _gameGui; + private readonly IObjectTable _objectTable; + private readonly PairUiService _pairUiService; public NameplateService(ILogger logger, LightlessConfigService configService, - INamePlateGui namePlateGui, IClientState clientState, - PairManager pairManager, - LightlessMediator lightlessMediator) : base(logger, lightlessMediator) + IGameGui gameGui, + IObjectTable objectTable, + IGameInteropProvider interop, + LightlessMediator lightlessMediator, + PairUiService pairUiService) : base(logger, lightlessMediator) { _logger = logger; _configService = configService; - _namePlateGui = namePlateGui; _clientState = clientState; - _pairManager = pairManager; + _gameGui = gameGui; + _objectTable = objectTable; + _pairUiService = pairUiService; - _namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate; - _namePlateGui.RequestRedraw(); - Mediator.Subscribe(this, (_) => _namePlateGui.RequestRedraw()); + interop.InitializeFromAttributes(this); + _nameplateHook?.Enable(); + Refresh(); + + Mediator.Subscribe(this, (_) => Refresh()); } - private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList handlers) + /// + /// 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 , + /// + private nint UpdateNameplateDetour(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex) { - if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen)) + try + { + SetNameplate(namePlateInfo, battleChara); + } + catch (Exception e) + { + _logger.LogError(e, "Error in NameplateService UpdateNameplateDetour"); + } + + return _nameplateHook!.Original(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex); + } + + /// + /// Determine if the player should be colored based on conditions (isFriend, IsInParty) + /// + /// Player character that will be checked + /// All visible users in the current object table + /// PLayer should or shouldnt be colored based on the result. True means colored + private bool ShouldColorPlayer(IPlayerCharacter playerCharacter, HashSet 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; + } + + /// + /// Setting up the nameplate of the user to be colored + /// + /// Information given from the Signature to be updated + /// Character from FF + private void SetNameplate(RaptureAtkModule.NamePlateInfo* namePlateInfo, BattleChara* battleChara) + { + if (!_configService.Current.IsNameplateColorsEnabled || _clientState.IsPvPExcludingDen) + return; + if (namePlateInfo == null || battleChara == null) return; - var visibleUsersIds = _pairManager.GetOnlineUserPairs() - .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) - .Select(u => (ulong)u.PlayerCharacterId) - .ToHashSet(); + var obj = _objectTable.FirstOrDefault(o => o.Address == (nint)battleChara); + if (obj is not IPlayerCharacter player) + return; - var now = DateTime.UtcNow; - var colors = _configService.Current.NameplateColors; + var snapshot = _pairUiService.GetSnapshot(); + var visibleUsersIds = snapshot.PairsByUid.Values + .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) + .Select(u => (ulong)u.PlayerCharacterId) + .ToHashSet(); - foreach (var handler in handlers) - { - var playerCharacter = handler.PlayerCharacter; - if (playerCharacter == null) - continue; + //Check if player should be colored + if (!ShouldColorPlayer(player, visibleUsersIds)) + return; - 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); + var originalName = player.Name.ToString(); - if (visibleUsersIds.Contains(handler.GameObjectId) && - !( - (isInParty && !partyColorAllowed) || - (isFriend && !friendColorAllowed) - )) - { - handler.NameParts.TextWrap = CreateTextWrap(colors); + //Check if not null of the name + if (string.IsNullOrEmpty(originalName)) + return; - 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); - } - } - } - } + //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()); } + /// + /// Converts Uint code to Vector4 as we store Colors in Uint in our config, needed for lumina + /// + /// Color code + /// Vector4 Color + 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); + } + + /// + /// Checks if the string has any forbidden characters/symbols as the string builder wouldnt append. + /// + /// String that has to be checked + /// Contains forbidden characters/symbols or not + 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; + } + + /// + /// Wraps the given string with the given edge and text color. + /// + /// String that has to be wrapped + /// Edge(border) color + /// Text color + /// Color wrapped SeString + 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(); + } + + /// + /// Request redraw of nameplates + /// public void RequestRedraw() { - _namePlateGui.RequestRedraw(); + Refresh(); } - private static (SeString, SeString) CreateTextWrap(DtrEntry.Colors color) + /// + /// Toggles the refresh of the Nameplate addon + /// + protected void Refresh() { - var left = new Lumina.Text.SeStringBuilder(); - var right = new Lumina.Text.SeStringBuilder(); + AtkUnitBasePtr namePlateAddon = _gameGui.GetAddonByName("NamePlate"); - left.PushColorRgba(color.Foreground); - right.PopColor(); + if (namePlateAddon.IsNull) + { + _logger.LogInformation("NamePlate addon is null, cannot refresh nameplates."); + return; + } - left.PushEdgeColorRgba(color.Glow); - right.PopEdgeColor(); + var addonNamePlate = (AddonNamePlate*)namePlateAddon.Address; - return (left.ToReadOnlySeString().ToDalamudString(), right.ToReadOnlySeString().ToDalamudString()); + if (addonNamePlate == null) + { + _logger.LogInformation("addonNamePlate addon is null, cannot refresh nameplates."); + return; + } + + addonNamePlate->DoFullUpdate = 1; } protected override void Dispose(bool disposing) { - base.Dispose(disposing); + if (disposing) + { + _nameplateHook?.Dispose(); + } - _namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate; - _namePlateGui.RequestRedraw(); + base.Dispose(disposing); } } \ No newline at end of file diff --git a/LightlessSync/Services/NotificationService.cs b/LightlessSync/Services/NotificationService.cs index 8709710..cedb58b 100644 --- a/LightlessSync/Services/NotificationService.cs +++ b/LightlessSync/Services/NotificationService.cs @@ -4,9 +4,14 @@ 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; @@ -23,7 +28,9 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private readonly INotificationManager _notificationManager; private readonly IChatGui _chatGui; private readonly PairRequestService _pairRequestService; - private readonly HashSet _shownPairRequestNotifications = new(); + private readonly HashSet _shownPairRequestNotifications = []; + private readonly PairUiService _pairUiService; + private readonly PairFactory _pairFactory; public NotificationService( ILogger logger, @@ -32,7 +39,9 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ INotificationManager notificationManager, IChatGui chatGui, LightlessMediator mediator, - PairRequestService pairRequestService) : base(logger, mediator) + PairRequestService pairRequestService, + PairUiService pairUiService, + PairFactory pairFactory) : base(logger, mediator) { _logger = logger; _configService = configService; @@ -40,6 +49,8 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ _notificationManager = notificationManager; _chatGui = chatGui; _pairRequestService = pairRequestService; + _pairUiService = pairUiService; + _pairFactory = pairFactory; } public Task StartAsync(CancellationToken cancellationToken) @@ -59,7 +70,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ { var notification = CreateNotification(title, message, type, duration, actions, soundEffectId); - if (_configService.Current.AutoDismissOnAction && notification.Actions.Any()) + if (_configService.Current.AutoDismissOnAction && notification.Actions.Count != 0) { WrapActionsWithAutoDismiss(notification); } @@ -104,7 +115,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ } } - private void DismissNotification(LightlessNotification notification) + private static void DismissNotification(LightlessNotification notification) { notification.IsDismissed = true; notification.IsAnimatingOut = true; @@ -208,10 +219,12 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Mediator.Publish(new LightlessNotificationMessage(notification)); } - private string FormatDownloadCompleteMessage(string fileName, int fileCount) => - fileCount > 1 + private static string FormatDownloadCompleteMessage(string fileName, int fileCount) + { + return fileCount > 1 ? $"Downloaded {fileCount} files successfully." : $"Downloaded {fileName} successfully."; + } private List CreateDownloadCompleteActions(Action? onOpenFolder) { @@ -257,8 +270,10 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Mediator.Publish(new LightlessNotificationMessage(notification)); } - private string FormatErrorMessage(string message, Exception? exception) => - exception != null ? $"{message}\n\nError: {exception.Message}" : message; + private static string FormatErrorMessage(string message, Exception? exception) + { + return exception != null ? $"{message}\n\nError: {exception.Message}" : message; + } private List CreateErrorActions(Action? onRetry, Action? onViewLog) { @@ -332,8 +347,9 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ return string.Join("\n", activeDownloads.Select(x => $"• {x.PlayerName}: {FormatDownloadStatus(x)}")); } - private string FormatDownloadStatus((string PlayerName, float Progress, string Status) download) => - download.Status switch + private static string FormatDownloadStatus((string PlayerName, float Progress, string Status) download) + { + return download.Status switch { "downloading" => $"{download.Progress:P0}", "decompressing" => "decompressing", @@ -341,6 +357,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ "waiting" => "waiting for slot", _ => download.Status }; + } private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch { @@ -391,6 +408,17 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ _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) { @@ -478,13 +506,16 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ }); } - private Dalamud.Interface.ImGuiNotification.NotificationType - ConvertToDalamudNotificationType(NotificationType type) => type switch + private static Dalamud.Interface.ImGuiNotification.NotificationType + ConvertToDalamudNotificationType(NotificationType type) { - NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error, - NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning, - _ => Dalamud.Interface.ImGuiNotification.NotificationType.Info - }; + 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) { @@ -568,7 +599,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _) { var activeRequests = _pairRequestService.GetActiveRequests(); - var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet(); + var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet(StringComparer.Ordinal); // Dismiss notifications for requests that are no longer active (expired) var notificationsToRemove = _shownPairRequestNotifications @@ -585,7 +616,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private void HandlePairDownloadStatus(PairDownloadStatusMessage msg) { - var userDownloads = msg.DownloadStatus.Where(x => x.PlayerName != "Pair Queue").ToList(); + 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); @@ -659,7 +690,14 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ { try { - Mediator.Publish(new CyclePauseMessage(userData)); + 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); @@ -734,7 +772,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ return actions; } - private string GetUserDisplayName(UserData userData, string playerName) + private static string GetUserDisplayName(UserData userData, string playerName) { if (!string.IsNullOrEmpty(userData.Alias) && !string.Equals(userData.Alias, userData.UID, StringComparison.Ordinal)) { diff --git a/LightlessSync/Services/PairProcessingLimiter.cs b/LightlessSync/Services/PairProcessing/PairProcessingLimiter.cs similarity index 91% rename from LightlessSync/Services/PairProcessingLimiter.cs rename to LightlessSync/Services/PairProcessing/PairProcessingLimiter.cs index 239ba75..1a860d3 100644 --- a/LightlessSync/Services/PairProcessingLimiter.cs +++ b/LightlessSync/Services/PairProcessing/PairProcessingLimiter.cs @@ -1,15 +1,12 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; -namespace LightlessSync.Services; +namespace LightlessSync.Services.PairProcessing; 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; @@ -24,8 +21,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(this, _ => UpdateSemaphoreLimit()); } @@ -88,7 +85,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase if (!enabled) { - var releaseAmount = HardLimit - _semaphore.CurrentCount; + var releaseAmount = _hardLimit - _semaphore.CurrentCount; if (releaseAmount > 0) { TryReleaseSemaphore(releaseAmount); @@ -110,7 +107,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase var increment = desiredLimit - _currentLimit; _pendingIncrements += increment; - var available = HardLimit - _semaphore.CurrentCount; + var available = _hardLimit - _semaphore.CurrentCount; var toRelease = Math.Min(_pendingIncrements, available); if (toRelease > 0 && TryReleaseSemaphore(toRelease)) { @@ -148,7 +145,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase private int CalculateLimit() { var configured = _configService.Current.MaxConcurrentPairApplications; - return Math.Clamp(configured, 1, HardLimit); + return Math.Clamp(configured, 1, _hardLimit); } private bool TryReleaseSemaphore(int count = 1) @@ -248,8 +245,3 @@ 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); -} diff --git a/LightlessSync/Services/PairProcessing/PairProcessingLimiterSnapshot.cs b/LightlessSync/Services/PairProcessing/PairProcessingLimiterSnapshot.cs new file mode 100644 index 0000000..64cc6b0 --- /dev/null +++ b/LightlessSync/Services/PairProcessing/PairProcessingLimiterSnapshot.cs @@ -0,0 +1,9 @@ +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); +} diff --git a/LightlessSync/Services/PairRequestService.cs b/LightlessSync/Services/PairRequestService.cs index 2531a3a..206fea3 100644 --- a/LightlessSync/Services/PairRequestService.cs +++ b/LightlessSync/Services/PairRequestService.cs @@ -1,6 +1,6 @@ using LightlessSync.LightlessConfiguration.Models; -using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; +using LightlessSync.UI.Services; using Microsoft.Extensions.Logging; namespace LightlessSync.Services; @@ -8,10 +8,11 @@ namespace LightlessSync.Services; public sealed class PairRequestService : DisposableMediatorSubscriberBase { private readonly DalamudUtilService _dalamudUtil; - private readonly PairManager _pairManager; + private readonly PairUiService _pairUiService; private readonly Lazy _apiController; private readonly Lock _syncRoot = new(); private readonly List _requests = []; + private readonly Dictionary _displayNameCache = new(StringComparer.Ordinal); private static readonly TimeSpan _expiration = TimeSpan.FromMinutes(5); @@ -19,12 +20,12 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, - PairManager pairManager, + PairUiService pairUiService, Lazy apiController) : base(logger, mediator) { _dalamudUtil = dalamudUtil; - _pairManager = pairManager; + _pairUiService = pairUiService; _apiController = apiController; Mediator.Subscribe(this, _ => @@ -96,6 +97,10 @@ 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) @@ -129,6 +134,23 @@ 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)) { @@ -138,8 +160,9 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase : name; } - var pair = _pairManager - .GetOnlineUserPairs() + var snapshot = _pairUiService.GetSnapshot(); + var pair = snapshot.PairsByUid.Values + .Where(p => !string.IsNullOrEmpty(p.GetPlayerNameHash())) .FirstOrDefault(p => string.Equals(p.Ident, hashedCid, StringComparison.Ordinal)); if (pair != null) @@ -185,7 +208,21 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase } var now = DateTime.UtcNow; - return _requests.RemoveAll(r => now - r.ReceivedAt > _expiration) > 0; + 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) @@ -229,4 +266,32 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase 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; + } + } } diff --git a/LightlessSync/Services/PerformanceCollectorService.cs b/LightlessSync/Services/PerformanceCollectorService.cs index 877cc1c..75fe736 100644 --- a/LightlessSync/Services/PerformanceCollectorService.cs +++ b/LightlessSync/Services/PerformanceCollectorService.cs @@ -26,12 +26,12 @@ public sealed class PerformanceCollectorService : IHostedService { if (!_lightlessConfigService.Current.LogPerformance) return func.Invoke(); - string cn = sender.GetType().Name + _counterSplit + counterName.BuildMessage(); + var owner = sender.GetType().Name; + var counter = counterName.BuildMessage(); + var cn = string.Concat(owner, _counterSplit, counter); 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 cn = sender.GetType().Name + _counterSplit + counterName.BuildMessage(); + var owner = sender.GetType().Name; + var counter = counterName.BuildMessage(); + var cn = string.Concat(owner, _counterSplit, counter); 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(new(TimeOnly.FromDateTime(DateTime.Now), elapsed)); + list.Add((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 = orderedData[0].Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0]; + var orderedData = data.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToList(); + var previousCaller = SplitCounterKey(orderedData[0].Key).Owner; foreach (var entry in orderedData) { - var newCaller = entry.Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0]; + var newCaller = SplitCounterKey(entry.Key).Owner; 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.Last().Item2).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15)); + sb.Append((" " + TimeSpan.FromTicks(pastEntries.LastOrDefault() == default ? 0 : pastEntries[^1].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.Last().Item1.ToString("HH:mm:ss.ffff", CultureInfo.InvariantCulture))).PadRight(15, ' ')); + sb.Append((" " + (pastEntries.LastOrDefault() == default ? "-" : pastEntries[^1].Item1.ToString("HH:mm:ss.ffff", CultureInfo.InvariantCulture))).PadRight(15, ' ')); sb.Append('|'); sb.Append((" " + pastEntries.Count).PadRight(10)); sb.Append('|'); @@ -157,6 +157,12 @@ 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, '-')); @@ -183,7 +189,7 @@ public sealed class PerformanceCollectorService : IHostedService { try { - var last = entries.Value.ToList().Last(); + var last = entries.Value.ToList()[^1]; if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !PerformanceCounters.TryRemove(entries.Key, out _)) { _logger.LogDebug("Could not remove performance counter {counter}", entries.Key); diff --git a/LightlessSync/Services/PlayerPerformanceService.cs b/LightlessSync/Services/PlayerPerformanceService.cs index 7db92e1..e77ccd7 100644 --- a/LightlessSync/Services/PlayerPerformanceService.cs +++ b/LightlessSync/Services/PlayerPerformanceService.cs @@ -1,9 +1,10 @@ using LightlessSync.API.Data; using LightlessSync.FileCache; using LightlessSync.LightlessConfiguration; -using LightlessSync.PlayerData.Handlers; +using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Events; using LightlessSync.Services.Mediator; +using LightlessSync.Services.TextureCompression; using LightlessSync.UI; using LightlessSync.WebAPI.Files.Models; using Microsoft.Extensions.Logging; @@ -17,20 +18,22 @@ public class PlayerPerformanceService private readonly ILogger _logger; private readonly LightlessMediator _mediator; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; + private readonly TextureDownscaleService _textureDownscaleService; private readonly Dictionary _warnedForPlayers = new(StringComparer.Ordinal); public PlayerPerformanceService(ILogger logger, LightlessMediator mediator, PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager, - XivDataAnalyzer xivDataAnalyzer) + XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService) { _logger = logger; _mediator = mediator; _playerPerformanceConfigService = playerPerformanceConfigService; _fileCacheManager = fileCacheManager; _xivDataAnalyzer = xivDataAnalyzer; + _textureDownscaleService = textureDownscaleService; } - public async Task CheckBothThresholds(PairHandler pairHandler, CharacterData charaData) + public async Task CheckBothThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData) { var config = _playerPerformanceConfigService.Current; bool notPausedAfterVram = ComputeAndAutoPauseOnVRAMUsageThresholds(pairHandler, charaData, []); @@ -39,37 +42,37 @@ public class PlayerPerformanceService if (!notPausedAfterTris) return false; if (config.UIDsToIgnore - .Exists(uid => string.Equals(uid, pairHandler.Pair.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pairHandler.Pair.UserData.UID, StringComparison.Ordinal))) + .Exists(uid => string.Equals(uid, pairHandler.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pairHandler.UserData.UID, StringComparison.Ordinal))) return true; - var vramUsage = pairHandler.Pair.LastAppliedApproximateVRAMBytes; - var triUsage = pairHandler.Pair.LastAppliedDataTris; + var vramUsage = pairHandler.LastAppliedApproximateVRAMBytes; + var triUsage = pairHandler.LastAppliedDataTris; - bool isPrefPerm = pairHandler.Pair.UserPair.OwnPermissions.HasFlag(API.Data.Enum.UserPermissions.Sticky); + bool isPrefPerm = pairHandler.HasStickyPermissions; - bool exceedsTris = CheckForThreshold(config.WarnOnExceedingThresholds, config.TrisWarningThresholdThousands * 1000, + bool exceedsTris = CheckForThreshold(config.WarnOnExceedingThresholds, config.TrisWarningThresholdThousands * 1000L, triUsage, config.WarnOnPreferredPermissionsExceedingThresholds, isPrefPerm); - bool exceedsVram = CheckForThreshold(config.WarnOnExceedingThresholds, config.VRAMSizeWarningThresholdMiB * 1024 * 1024, + bool exceedsVram = CheckForThreshold(config.WarnOnExceedingThresholds, config.VRAMSizeWarningThresholdMiB * 1024L * 1024L, vramUsage, config.WarnOnPreferredPermissionsExceedingThresholds, isPrefPerm); - if (_warnedForPlayers.TryGetValue(pairHandler.Pair.UserData.UID, out bool hadWarning) && hadWarning) + if (_warnedForPlayers.TryGetValue(pairHandler.UserData.UID, out bool hadWarning) && hadWarning) { - _warnedForPlayers[pairHandler.Pair.UserData.UID] = exceedsTris || exceedsVram; + _warnedForPlayers[pairHandler.UserData.UID] = exceedsTris || exceedsVram; return true; } - _warnedForPlayers[pairHandler.Pair.UserData.UID] = exceedsTris || exceedsVram; + _warnedForPlayers[pairHandler.UserData.UID] = exceedsTris || exceedsVram; if (exceedsVram) { - _mediator.Publish(new EventMessage(new Event(pairHandler.Pair.PlayerName, pairHandler.Pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning, + _mediator.Publish(new EventMessage(new Event(pairHandler.PlayerName, pairHandler.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.Pair.PlayerName, pairHandler.Pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning, + _mediator.Publish(new EventMessage(new Event(pairHandler.PlayerName, pairHandler.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning, $"Exceeds triangle threshold: ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)"))); } @@ -78,41 +81,40 @@ public class PlayerPerformanceService string warningText = string.Empty; if (exceedsTris && !exceedsVram) { - warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured triangle warning threshold\n" + + warningText = $"Player {pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeds your configured triangle warning threshold\n" + $"{triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles"; } else if (!exceedsTris) { - warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured VRAM warning threshold\n" + + warningText = $"Player {pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeds your configured VRAM warning threshold\n" + $"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB"; } else { - warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds both VRAM warning threshold and triangle warning threshold\n" + + 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"; } _mediator.Publish(new PerformanceNotificationMessage( - $"{pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds performance threshold(s)", + $"{pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) exceeds performance threshold(s)", warningText, - pairHandler.Pair.UserData, - pairHandler.Pair.IsPaused, - pairHandler.Pair.PlayerName)); + pairHandler.UserData, + pairHandler.IsPaused, + pairHandler.PlayerName)); } return true; } - public async Task CheckTriangleUsageThresholds(PairHandler pairHandler, CharacterData charaData) + public async Task CheckTriangleUsageThresholds(IPairPerformanceSubject 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? playerReplacements)) { - pair.LastAppliedDataTris = 0; + pairHandler.LastAppliedDataTris = 0; return true; } @@ -126,35 +128,35 @@ public class PlayerPerformanceService triUsage += await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false); } - pair.LastAppliedDataTris = triUsage; + pairHandler.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, pair.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pair.UserData.UID, StringComparison.Ordinal))) + .Exists(uid => string.Equals(uid, pairHandler.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pairHandler.UserData.UID, StringComparison.Ordinal))) return true; - bool isPrefPerm = pair.UserPair.OwnPermissions.HasFlag(API.Data.Enum.UserPermissions.Sticky); + bool isPrefPerm = pairHandler.HasStickyPermissions; // now check auto pause - if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.TrisAutoPauseThresholdThousands * 1000, + if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.TrisAutoPauseThresholdThousands * 1000L, triUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm)) { - var message = $"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured triangle auto pause threshold and has been automatically paused\n" + + 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( - $"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused", + $"{pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) automatically paused", message, - pair.UserData, + pairHandler.UserData, true, - pair.PlayerName)); + pairHandler.PlayerName)); - _mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning, + _mediator.Publish(new EventMessage(new Event(pairHandler.PlayerName, pairHandler.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning, $"Exceeds triangle threshold: automatically paused ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)"))); - _mediator.Publish(new PauseMessage(pair.UserData)); + _mediator.Publish(new PauseMessage(pairHandler.UserData)); return false; } @@ -162,16 +164,18 @@ public class PlayerPerformanceService return true; } - public bool ComputeAndAutoPauseOnVRAMUsageThresholds(PairHandler pairHandler, CharacterData charaData, List toDownloadFiles) + public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List toDownloadFiles) { var config = _playerPerformanceConfigService.Current; - var pair = pairHandler.Pair; + bool skipDownscale = pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions; long vramUsage = 0; + long effectiveVramUsage = 0; if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List? playerReplacements)) { - pair.LastAppliedApproximateVRAMBytes = 0; + pairHandler.LastAppliedApproximateVRAMBytes = 0; + pairHandler.LastAppliedApproximateEffectiveVRAMBytes = 0; return true; } @@ -183,11 +187,13 @@ 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 { @@ -201,39 +207,63 @@ 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; } - pair.LastAppliedApproximateVRAMBytes = vramUsage; + pairHandler.LastAppliedApproximateVRAMBytes = vramUsage; + pairHandler.LastAppliedApproximateEffectiveVRAMBytes = effectiveVramUsage; _logger.LogDebug("Calculated VRAM usage for {p}", pairHandler); // no warning of any kind on ignored pairs if (config.UIDsToIgnore - .Exists(uid => string.Equals(uid, pair.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pair.UserData.UID, StringComparison.Ordinal))) + .Exists(uid => string.Equals(uid, pairHandler.UserData.Alias, StringComparison.Ordinal) || string.Equals(uid, pairHandler.UserData.UID, StringComparison.Ordinal))) return true; - bool isPrefPerm = pair.UserPair.OwnPermissions.HasFlag(API.Data.Enum.UserPermissions.Sticky); + bool isPrefPerm = pairHandler.HasStickyPermissions; // now check auto pause - if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.VRAMSizeAutoPauseThresholdMiB * 1024 * 1024, + if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.VRAMSizeAutoPauseThresholdMiB * 1024L * 1024L, vramUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm)) { - var message = $"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured VRAM auto pause threshold and has been automatically paused\n" + + 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 PerformanceNotificationMessage( - $"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused", + $"{pairHandler.PlayerName} ({pairHandler.UserData.AliasOrUID}) automatically paused", message, - pair.UserData, + pairHandler.UserData, true, - pair.PlayerName)); + pairHandler.PlayerName)); - _mediator.Publish(new PauseMessage(pair.UserData)); + _mediator.Publish(new PauseMessage(pairHandler.UserData)); - _mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning, + _mediator.Publish(new EventMessage(new Event(pairHandler.PlayerName, pairHandler.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning, $"Exceeds VRAM threshold: automatically paused ({UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB} MiB)"))); return false; diff --git a/LightlessSync/Services/Profiles/LightlessGroupProfileData.cs b/LightlessSync/Services/Profiles/LightlessGroupProfileData.cs new file mode 100644 index 0000000..2866955 --- /dev/null +++ b/LightlessSync/Services/Profiles/LightlessGroupProfileData.cs @@ -0,0 +1,17 @@ +namespace LightlessSync.Services.Profiles; + +public record LightlessGroupProfileData( + bool IsDisabled, + bool IsNsfw, + string Base64ProfilePicture, + string Base64BannerPicture, + string Description, + IReadOnlyList Tags) +{ + public Lazy ProfileImageData { get; } = new(() => ConvertSafe(Base64ProfilePicture)); + public Lazy BannerImageData { get; } = new(() => ConvertSafe(Base64BannerPicture)); + + private static byte[] ConvertSafe(string value) => string.IsNullOrEmpty(value) + ? Array.Empty() + : Convert.FromBase64String(value); +} diff --git a/LightlessSync/Services/Profiles/LightlessProfileData.cs b/LightlessSync/Services/Profiles/LightlessProfileData.cs new file mode 100644 index 0000000..ef62862 --- /dev/null +++ b/LightlessSync/Services/Profiles/LightlessProfileData.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace LightlessSync.Services; + +public record LightlessProfileData( + bool IsFlagged, + bool IsNSFW, + string Base64ProfilePicture, + string Base64SupporterPicture, + string Base64BannerPicture, + string Description, + IReadOnlyList Tags) +{ + public Lazy ImageData { get; } = new(() => ConvertSafe(Base64ProfilePicture)); + public Lazy SupporterImageData { get; } = new(() => ConvertSafe(Base64SupporterPicture)); + public Lazy BannerImageData { get; } = new(() => ConvertSafe(Base64BannerPicture)); + + private static byte[] ConvertSafe(string value) => string.IsNullOrEmpty(value) ? Array.Empty() : Convert.FromBase64String(value); +} diff --git a/LightlessSync/Services/Profiles/LightlessProfileManager.cs b/LightlessSync/Services/Profiles/LightlessProfileManager.cs new file mode 100644 index 0000000..fd8c19c --- /dev/null +++ b/LightlessSync/Services/Profiles/LightlessProfileManager.cs @@ -0,0 +1,348 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Comparer; +using LightlessSync.API.Dto.User; +using LightlessSync.LightlessConfiguration; +using LightlessSync.Services.Mediator; +using LightlessSync.Services.Profiles; +using LightlessSync.WebAPI; +using Microsoft.Extensions.Logging; +using Serilog.Core; +using System; +using System.Collections.Concurrent; + +namespace LightlessSync.Services; +public class LightlessProfileManager : MediatorSubscriberBase +{ + public LightlessGroupProfileData LoadingProfileGroupData => _loadingProfileGroupData; + //Const strings for default values meant in the profile screen. + private const string _lightlessLogo = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDIgNzkuZGJhM2RhMywgMjAyMy8xMi8xMy0wNTowNjo0OSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI2LjggKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyNS0wOC0zMFQwMDo1MTozNCswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNS0wOC0zMFQwMjoxMjo0MiswMjowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjUtMDgtMzBUMDI6MTI6NDIrMDI6MDAiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Y2JjY2YwZDctNTU4ZS1kYzQ1LTk0MmQtZTg5YWE5ZWRhYjUzIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6Nzk1NDcwMjYtYjM2OC1jMjRhLTliYzEtYmQzMjE4OTJlN2FiIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MGRiOTU0NTktM2E2OS0xNDQ2LWFmYmMtM2M4ODhjZDUxYmU0IiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MGRiOTU0NTktM2E2OS0xNDQ2LWFmYmMtM2M4ODhjZDUxYmU0IiBzdEV2dDp3aGVuPSIyMDI1LTA4LTMwVDAwOjUxOjM0KzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjYuOCAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmE5OWYwOGE5LTgxNDItY2E0Mi04MmY1LTM2MjZmOTgzNDZhYiIgc3RFdnQ6d2hlbj0iMjAyNS0wOC0zMFQwMDo1MTozNCswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI2LjggKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpjYmNjZjBkNy01NThlLWRjNDUtOTQyZC1lODlhYTllZGFiNTMiIHN0RXZ0OndoZW49IjIwMjUtMDgtMzBUMDI6MTI6NDIrMDI6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS43IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5CIHRlAAEVd0lEQVR42uz9d7xtWVXmD3/HnHOttcPJ5+ZYOVAUFDlnEBQwKybs1jaDoj9b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"; + private const string _lightlessLogoLoading = "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"; + private const string _lightlessLogoNsfw = "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"; + private const string _lightlessSupporter = "iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAAAXNSR0IB2cksfwAAAARnQU1BAACxjwv8YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+kKFxILGwzS8FQAABd6SURBVHja7Zx3nBRVtsfPvRU6h4nAADNkZpCgg4hIkpwFVhR2BVeUFcPqqhs++3b3rb5dfW+TD9dFFDCwIIqC5CwMMCMCikoSkDhDnNzTuSvce98f3dVd1T3gEAb0OffzqU/19FTdvvWtc2+dc+7vFkBTaSpNpak0labSVG5OQd+Vhmz+aG+7yvN1w4JeqSfH466RoOy0u8wd/Z6ISAkDhAFsbrMihdVSWSI+m8t0nlJ0nhB2KKOF/Ui/YV2+apmXXveDArh15d4OZUcqH0CI3eevDt+CEAOEABCwaMP0rWMADAAYIGAMgDIENL5HwBAGq9t8jBO4rRabuHHak/03chyW/98BZIzxy9/cPtlbGZgZ8kb6Y8QAIwYYGCQARiGmnBuDxwCAMQQUovAoRUBYbKMIVIZAMAuetGzb263bp704fPytdd97gAFf2LT67R0zwn7pt3Ig0gojBpwGLwZO2ycaxgxNZaDBiwFkCWuMAsRAqAYRg0oRcCbhTL9RHW/rc3fn2sa4Lv5GwFvz7+L7Pnhl01+VkNSWQxRMvBEeRgBI20MyxGgR7eYLiOcPYJ47EfKGDzfPb1VXfqLiJOJ4UnBXAWvVOYf5agNof/ExVHqkEjp0z86srQrnMoSyBB5/P7vwJ+u/an1y/9m5YW94FI8ocJgBhylwiAGHADDWW6AGMAqTN4kVJqd1AyC0oXX3jjs73tH1/A/qKbxi/tYfeS543yERycljBjyOAUQMuBg4bY8QAwwAnMiFRbv1A1dO1ruF4wZt/cG6MR+9vvmvNWdqf8MjCjzHgEMUeBwFxmNqgIcRAG/i6swux6zs/HavdexTWPOD9QMZZcKiv6x6W/KHpwpcFBqPE/tky+MFrJidtlfdeS3/3GVof+/30ZG+bg8RSin34avrlyiB8I9EjkIyQE7rwjhqkSaHdZ+9efa0HuNHHGpI/cGqKmdZSUlXNRLpAojLQhyXF6yqRYjnwZyZFQlW1hy3ZGd5sCB83X3csIMIIfK9ssD3/r56ftgTmCFwFATMYgCN8PjYA8SalT77tikTfsWLonQZfxGd2rBmSKSmejQDGKkGQwWMUmAMxf1AluRMUwZAKAIkmlXG8XuA44vS8lqu7TGy/2ffaYCr53/8XPXpqpcFneUlA+QxA0HEzJzmfLzn1MlzL1XX+U+2ZYcunntKDQZnECnSHOm8QRaPSKKwEhATG6EICAMgNOoTqhSD4LCXIl6Y17yg7bwu/a/vGHvNADct3nF7+bHyTzFVBA2ggKMPjyjAqPUJJo6ld2j7UP6IoQvrq+fi7mJX6MKZF5S6mplAiQW0cE7vTuutD0AHL2aFFOsgojhAlSJQCQbGCUFLhntuVodWL3YfWOi56QDliGJaNmv1PjUUzheT4AkavJhF2ptnz+w+edK8+uopW7NkglxXM5fJkWwEDABdOhLRLDARjSRAkhhEEgvvVIqAaAApjm4EA5jMHmuG67mh00cvuKkAP3p17a+Dld6/aZYnciQBT9eFrVnp/+zxkynP1DPOcWXLF8xS6mqeijvSSIt66xkXDdanfdZbIAIaC+eilohjEKMWqMQgKiS6mdyONc06tZx2x8je3hsOcF/JwfSvtx8+jVTZGbe+uAUm4Jmd1n3dp03tzfG8ITOihAKWi5uXL1c8FSMxYvEIRL+/PMDoPp6N0bpybOzTrFGNj4UJiAqJWqJMMGCrtdSW6Rw+cvqI4zfUjSndX/ZzpihOQTfWGTcGgoiJJSPtwWR4jDF0buWCJcRbNZLXohEDQGP3xaLVB7y4n6nqEWSxn1L9Ho/QvB0CzEH47AnA9rQCVZIKqKL2ViXJhRACRDEQoFoNmrMVT0hE/0Igh8JtfJVsz2eb9o65Y8Ttu26IBVJC8bJ/fHSRhsPZIkchaoEEhCT/z+yyz+n24ENPJp9/buU7/0O8lb/VwGnhnAGi2X4S8eK7Qov2q113DP+ygakydGHbuoFSTdUUOeD/CZGJI2qB2PAwUXTdWCEYJJUDbLX4crvnDuwz6o59jQ5ww1ubxnrOVK4RY+OeEIPI690YEUXSC7q0zRtwd7n+3IqilXdL509sw0w1JhEQAwQMsMXxBedu8Uf3oPvXX8v4XLX/c7f3+NFnZK/3l4pM7CQ+/iHDOKgQDiSCQVYxECSc69y38129h992tqG/c1V5HiUcmczpwjNDpIGin0WrZXEyPMYYp1afe50DNRGVxCITjudCQlbe4xkTf3H7tcIDAMjq0auuw6RpLzg7FuSbHPa1GNO4P2r47XioyQAoaXXii9PLIiHJ3KgA5WBkGI+MjUiAjEK0t2r9ZvJ5lesXTQXJn48RBW3jEAPOZC4TmnW43T1s2hvXO1JoNWDI+fypj4yz5bR6luOxgnUZIcMNjO3lQOSOla9v/nOjAdyzfncnEpaaxfN6uhgXIwYYUxBs5rK8QUN3p4xRYf8zHNKltDADzmIrM3XsNcA5cNKRxoxZO46/9xWT2z1ctJiC8Z6i3cAkqwzVhX+18f1P+l0zwLVLv8hL/s5X4ekVB4aNmWUNKi+Ka5PP8+zaeAuLeG+NQo4dL5gC2JEx1Np94Jlrmi7YveaRujWz36jb9M54KoWFSx1XMPmB7ekFXQYLFpPHcA1JeUogKlScrpnHGOOvGuDC14qHH/2q4v0UKyJqp3haKiktr33mzabi5PPU6nPj9fAQYsC5sh93DnnwxLVal/3OcW9hq3s1DdTM8qyZc6Fm7fzf+w/utNV3bOu7+n6WXlAwhjcJQYwgcQ3xTHm0jRFfuODD17Y8dtUAA17pAUUi1pTvK70t4tDidzABD2MG9ty2B1MqVCN94u4KYsBZ3TscQx9693p1UeeQqetNbW/thq3OtdRX+WL46OffVH+8ZGx9x+b17btLsFnv4QVOrc8ANKgBT+j5w1+W2q4KYCSs9iIKUZK/t6VZMvElLA8jBpjnoFnPO1PGM8RoAUYAGKJTmJy7xe+v9zhnKxwaTBv9s+mcI+NJIFJLpeL0mvJV82cTWTIlH9tj6gNFlqyMZ4zXEu3G2gyh5Jcyvyw+NuOqABKF5vM84pK/N9vNLZPBId3UpGh31DsHixhpp1kftjj22vrft7OxHhjp4x6bI2TlPYoxAPVVP1m+8s2PK4pWOpKP637/va9ZMtMXps4OJq4v5JOeu2KAFRfqWkZCKnK6RVeq580wQrqIARIzaQgxoFK4vP47IiFNcYDMjg8aO1OcNmTKfM7V7A8IMWARf3+p/EzRxa2rUiBmdGj7BG82ndA79XrDCPuk3A/nFY++IoAfzP+sOQAAL6DcVA5KBYLkuxVTFET39UY3nMXhR7FsC5eWs/ZGpNszxzz8ErY41yEEwJTw7VLluXWyv87gJLfpc0fQ3bb1Y/GJfUjE49o1equDP70igNmt7C4EAL7KEM8YMxwTrvVdRLoJcIQAQPfDnNnc4hLhy1EEDJDJHrD0GnP0Rk36WDrcOgPzghcBAI2E+l/YvPKV5GO6jh661ex2LtdDi15PdLo1HFTGBP0RU4MBmsx8G61L7tt5LN/gMmS7/Ajpk54Qv2MIMSD+OiejJKVeJFr2Rs9hNwweAICzR79ybHP9l3Y9ird6Ztm6peOTj3M0z/pPzOFoLhKBbmMgh1Tbmvf2DmowwJOHqnkNzJnj5S2N3oh8JCH+YYbcXTQLRaB6z7ZuKQAFcWP0/+wI3OCSPvi+uVg0V2njteypnlN14AuDe9J97JDDJqdtXULcpEupIQY1lcEBDQYY9CfSd95Kf4HhjjZP/zoORUcuoaZCIFdf7JFyh4dO34gEczUyO264REOwu0JItM6Pj+ORUI73+NFfJh8n2q2vJ6eqtI2otPeVx8LRs3vqv+o27K69vEnwp+aKE5+ZIg1OqYrjZezI/guI1u1wE4rYLO9dfVvlOs+z1Ue+NjyV7/zx2A28SagyWl/CdWwwQKtdjP8WI7SPYXy0WWXRbllz6bQiAiqFRjHGUp7GtqHTX7YNfnDTzQCY2XfEEWy2ntHaSlXVXX1w/0OGq0CIig7bOlaPbfjr5IxQULY1CGCHbpmq9jnkjXTcu3W/4clqcTneMhgeM9oglSLZlVs+GgnftYI4g/OuSlJKlIEQWqefBdRrEnds/LpzA8dA6VR8EocCnD12cbj+/z0njS4SbNbD0elFbfQzakqJr+bJ7xw/k+WQfkJK8fu7nyoq6mSIkws770rMm8Q6Uawz7dla2rAu7Eqz+rQpQwYAYX/k3hRXx2n7OzPMlmlKgejfJOgdU719ZeF3CSBV1SpDr2EIQtU1Bpemfc8u50WbpTx6PaAzEIC8/DRrgwAOHtP1MKBEBXJIGbWv+ECG/pjCKRMX8hbzMcYgphjQS3ARMMZA9VTM+i4BtOZ1lhO9JtpeORDqkwJF5I9rrjTVjAMApLBKGgTQZjdFbC7xAmXRCmSJ8KUHzz5oOBFjas9p/mu9VlnTqmg/qPo9A6o3v//wd8YCKYu3TVM0EEJTfNawL3I0qr1OiNkBAMrL/HKD3RiOwwcoQBxO2Bd+Kjms6zpu1GrR7VqhV0sxgLh6ngGAUnPh1ZotH3S6EYACWxY0u9z/g6e/ERM3ObqpstIh+Th3q8w6mqT+AgAYNL6z2mCAvIB365VPkYDcduPbm6amuAdd8mciPmqthnFQO1dVbKq3aoP/0J6MRoVXtGgQI8prl32IiOYWGhANDpEJEFXN1h9Xe95TyvRrUGLfDxhecLTBAFu3Tys2yMYYAl+F748hf8gQVOf26llla958CmBONsosdI0M+duFDhWv9X6+xdUY8EKfretGfJUrqBQqu9xxaiTcLVVTiMBTdt5guZGAosbFSrHrcKabyhFCUoMBjplUuMNk5T0JnQmCSEhpX7RoS0oIVDDhnhJ7bt7jDHExgU9qI6mq3CmfO7rb8/HiNtcTXrDkw37K+cM7qBR04YzcJZc7lihqX5qkJ6QMgCGjz0908IhmgQjtu6JQDnOYOtJMK/V6O8oQ+Kv9f9i5fHuKQ9lpzJi3BZd7JsN8/K6RZPFj0Jev1lXs92xa+ONrBccYw/7Nb/2HWn58K5XDaciSvsrRd+Lnlzr+wq7inmpEakF0OkLKAAjDkJ6bU214WqfbLIThePsBAHge77riWDg7x/Hv+AogTbCoMEt1adViKZSaH7vlx1Pn2XLbTqXAhYkGkRqHASJFnErN2fdqVs0p8pWs6H11Vrd0gm/Vy/uI5/x/U0JExlsCXLN2T192jCw79QBlWCfCjMqBKWDAHFdujLRsbbQVTxrANp3St10xwB9N673DnmY6qAkVtaVUQW+k57ZFG1+p75z2I8csdrTrNBAJltK4Ti+m1UuIIBGoAc8g6ezXu2tXvrq7bvOCXwT2bmp3GWtDob2bbvet+dfz3lWzTioXjqygkVA3yhBQxAOXnjvD0Xv0JeeWK77cY1eCwel65apmGILdVosQMoS/VWd9Dm3NHQMAi12oGj2p8JN6H7bfdrezcux/La2LvKvGNHY8xkAoA1+577ENs5cdGvXzSSlPvjZDR35+esPqbqq/7mUaqH2UUjV6qygGwFR35xCwkL83Dvt6k5qzr3hWzKoBgFNYtJ6nodpazpnVkvqrs7wr/7crk4Oipk4FnTMMvPkx16DJl51j8Z469Zwiq+6oBSZuKKEImMq+SUl/WU23qEEZKI1an8UmLEmG3LB0FgA8MLPfYkeG5aCqk4hpgsWwLzx790db6532azvqnkDH+x+cacpu1Qvb3MXGhuP4BVCqWQQGNRzMIJFgL9VXNYGq6sOK5+IIoiqFRAqJmvo0PiYDF+GadXgkc9Jzcy/X/nM7i/OkOs9vaFzmhuILEglFAJj/KvkcWWH5Kk040O27ZL5zRXMiySUnz/UkRUlaY4pAlhlUHL8wr2TR2kcvdW6bsZP2tp8yY6C5VfuByOpeRbDI4ktT4ysscRwi0QElFIN24fr/MdFxRGjRqW/a4Mlvf1vbvSePv6VIxBa/+UyvF8SABNHQNYvX72vvq1PcJDb2ubMsn4yceNtX1wRw0k/vLMnIsc9RaUJnrG2SRFHtmeq5JYvW/OlydbQeMbG43eSfTbB36N6KS8v5LZhdnxIkxC4q0aWSYekthomOM9id81TWvU/3SLv73m8VXR5dtuQlyR8akqgbASGJOhnmoUV+XpHBYk/XDlR1D4+0TMufvi3n3KCyeskXthMHyw+SkNTWzBMwcQREnoCmUBU5Cla3eUNez/yHO955W3lD6vSdOJzpO7x3AJMjfRElvRCH2jE5nAOqghDHATZZK4DRE4DQTiGz5ceZA8dvaWh7Dy9Z/GiktnauSnQaaZ0+WqEYsNW6fdyzkw2TRa//ecPymip5IgOAjObWT5743bD+1wUgAMDSd3bdVXakqgSrCjbFIUaF5ZpS1Wzla1056c/0njx20c1KGhxatOBxORCao6pMN2bH4NGEwNzZMvPh4Y+MiY9vh74odW9aeqg8FCImwcRB2/z0wsmP3PXV5X7rivSB903v82lGS+fzcY1xvFEo+pliiITU9NrT5QuL576//Ytl67vdSHByIGD++v335kS8gTmKwhJjNjHCUwgGbDFX9xzZ26A+273t5AOhEDEBALgyzP/8NnhXDBAAYPov7n7R2cy+TqYYZMIZxNp6qAFPaKDnTPmBnW++v/Do1pJGz8Z8s3ZN4eEPPtgTrPY8riQtZ1Bi4NR4OxHwFvNLGS0zI/HwTaXIVyc/CwBgdQgnuxa2bJD46apE5u+8usMWrAvvCXtCt5i0cRATuNRSL9HEgWg1LbNnp83uMWH0jusJrrSkpJ33dOkflGBouqoQIBSSFtlgw/inEAzIJB4b//Q9PSx2Sxzg3L9tfbjyXOAtTsBy5x7Z/e59sPfnjQYQAGDLmgO5X+4oLcFEzTXxMaV+DOKlFhtymILZaT3JcWhRWtvcFZ0GDzhwVYlRQvgjK1YOl/2BR9SINFGVVaQlL0jMX0w8vRNDjEowUMyzFvk5gwZPHhi/kVUVXuu7sz89GfDKzVt3cD/50NMD5zR4nuVa7v78l4vaey4GijFRc0SegIipbq0ITVlsrX9nAkYMRKvpIi8KJZjn9og26/7sLp3PNyvofAwhRPW/U3PiZIfy/Qdaq5FITzUi9aWKOkiVZJd+pRLVLE/vSxqsL7p35aT9acITo5/X1//my0U/v1jm/1dOW+frjzw76Ikrmqi61i701qxt7avP+YswVXPj60biy71SV6xrElpOJ/dNXqnEW0wg2KxAIhFQQ6FYPKybatQvNITEe2OoLnmRstCQILBmOJbe/9w99ydfwz9+t77YnWE+OOOXg694JvG6rBdet/TL3NNHKjeEPKEucStMgshhBrxO1Y/ree0JiilYjVKRRFPj8HRvL9InSBPwEr4fYVF46bkZW++e1GesO9MZSW7/sgV7cic91PuqhO7XbcX6v2fvsIX98mJvRWC8oIeYvOQfMYMlGvSFYNynZmX0by9KWupK0SXXClvctoWTnh7xqNlikq77XPP1rnDxGyXPnjte+xKmxJKAGHuQoNQXTxgVoXqISfDqgZicWSbJmRaOVzLz0n8/8WdD/t5ok/WNUenyRXs6Xyyrm+evDg0QcNLLJ1D9XTih4NfJy/QZJJY875z6ArJErg+DyWE+kJWb9tDYaQO+gkYsjfrmoo8W7p585puaF+SgnM/r16khqlufoRsHDdbHkl/eZhgHtdkympTxFqymKqvL/MKUp4e/gTGmje3AN/rLxxhjaOmCXVOqzvme8teE+/CGMZBGXwEVf3sRM6xY14+DjCUg6ie8NXgWl/mUySq+2m909zfbdG4RvFHh4w19/d3WtQfyjx0o/wkjdELAE+6Gmc4CQfcGt1jLki1Q/+o7xhCYHaZzJiu/Or25a+m4aXdtvxmJi5v2AsbPSo43+2b/hb7hgFRICckP+eQ2znRTm7BPzpBCSrwrm+0CWOxilbcqdMaVZb0AAMdMVmFfs1Zpnw6ZUHgKmsqlu34ThabSVJpKU2kqTaWpXLr8Hxvq6PyAt/YkAAAAAElFTkSuQmCC"; + private const string _lightlessBanner = "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"; + private const string _noUserDescription = "-- User has no description set --"; + private const string _noGroupDescription = "-- Syncshell has no description set --"; + private const string _nsfwDescription = "Profile not displayed - NSFW"; + private const string _loadingData = "Loading Profile Data from server..."; + private readonly ApiController _apiController; + private readonly ILogger _logger; + private readonly LightlessConfigService _lightlessConfigService; + private readonly ConcurrentDictionary _lightlessUserProfiles = new(UserDataComparer.Instance); + private readonly ConcurrentDictionary _lightlessGroupProfiles = new(GroupDataComparer.Instance); + + private static readonly int[] _emptyTagSet = Array.Empty(); + private readonly LightlessUserProfileData _defaultProfileUserData = new( + IsFlagged: false, + IsNSFW: false, + Base64ProfilePicture: _lightlessLogo, + Base64SupporterPicture: string.Empty, + Base64BannerPicture: _lightlessBanner, + Description: _noUserDescription, + Tags: _emptyTagSet); + + private readonly LightlessUserProfileData _loadingProfileUserData = new( + IsFlagged: false, + IsNSFW: false, + Base64ProfilePicture: _lightlessLogoLoading, + Base64SupporterPicture: string.Empty, + Base64BannerPicture: _lightlessBanner, + Description: _loadingData, + Tags: _emptyTagSet); + + private readonly LightlessGroupProfileData _loadingProfileGroupData = new( + IsDisabled: false, + IsNsfw: false, + Base64ProfilePicture: _lightlessLogoLoading, + Base64BannerPicture: _lightlessBanner, + Description: _loadingData, + Tags: _emptyTagSet); + + private readonly LightlessGroupProfileData _defaultProfileGroupData = new( + IsDisabled: false, + IsNsfw: false, + Base64ProfilePicture: _lightlessLogo, + Base64BannerPicture: _lightlessBanner, + Description: _noGroupDescription, + Tags: _emptyTagSet); + + private readonly LightlessUserProfileData _nsfwProfileUserData = new( + IsFlagged: false, + IsNSFW: true, + Base64ProfilePicture: _lightlessLogoNsfw, + Base64SupporterPicture: string.Empty, + Base64BannerPicture: string.Empty, + Description: _nsfwDescription, + Tags: _emptyTagSet); + + private readonly LightlessGroupProfileData _nsfwProfileGroupData = new( + IsDisabled: false, + IsNsfw: true, + Base64ProfilePicture: _lightlessLogoNsfw, + Base64BannerPicture: string.Empty, + Description: _nsfwDescription, + Tags: _emptyTagSet); + + private const string _noDescription = "-- Profile has no description set --"; + private readonly ConcurrentDictionary _lightlessProfiles = new(UserDataComparer.Instance); + private readonly LightlessProfileData _defaultProfileData = new( + IsFlagged: false, + IsNSFW: false, + Base64ProfilePicture: _lightlessLogo, + Base64SupporterPicture: string.Empty, + Base64BannerPicture: _lightlessBanner, + Description: _noDescription, + Tags: _emptyTagSet); + + private readonly LightlessProfileData _loadingProfileData = new( + IsFlagged: false, + IsNSFW: false, + Base64ProfilePicture: _lightlessLogoLoading, + Base64SupporterPicture: string.Empty, + Base64BannerPicture: _lightlessBanner, + Description: _loadingData, + Tags: _emptyTagSet); + + private readonly LightlessProfileData _nsfwProfileData = new( + IsFlagged: false, + IsNSFW: false, + Base64ProfilePicture: _lightlessLogoNsfw, + Base64SupporterPicture: string.Empty, + Base64BannerPicture: string.Empty, + Description: _nsfwDescription, + Tags: _emptyTagSet); + + public LightlessProfileManager(ILogger logger, LightlessConfigService lightlessConfigService, + LightlessMediator mediator, ApiController apiController) : base(logger, mediator) + { + _logger = logger; + _lightlessConfigService = lightlessConfigService; + _apiController = apiController; + + Mediator.Subscribe(this, (msg) => + { + if (msg.UserData != null) + { + _logger.LogTrace("Received Clear Profile for User profile {data}", msg.UserData.AliasOrUID); + _lightlessUserProfiles.Remove(msg.UserData, out _); + _lightlessProfiles.Remove(msg.UserData, out _); + } + else + { + _logger.LogTrace("Received Clear Profile for all User profiles"); + _lightlessUserProfiles.Clear(); + _lightlessProfiles.Clear(); + } + }); + + Mediator.Subscribe(this, (msg) => + { + if (msg.GroupData != null) + { + _logger.LogTrace("Received Clear Profile for Group profile {data}", msg.GroupData.AliasOrGID); + _lightlessGroupProfiles.Remove(msg.GroupData, out _); + } + else + { + _logger.LogTrace("Received Clear Profile for all Group profiles"); + _lightlessGroupProfiles.Clear(); + } + + }); + + Mediator.Subscribe(this, (_) => + { + _logger.LogTrace("Received Disconnect, Clearing Profiles"); + _lightlessUserProfiles.Clear(); + _lightlessGroupProfiles.Clear(); + _lightlessProfiles.Clear(); + } + ); + } + + /// + /// Fetches User Profile from cache or API + /// + /// User Data of given user + /// LightlessUserProfileData of given user + public LightlessUserProfileData GetLightlessUserProfile(UserData data) + { + if (!_lightlessUserProfiles.TryGetValue(data, out var profile)) + { + _logger.LogTrace("Requesting User profile for {data}", data); + _ = Task.Run(() => GetLightlessProfileFromService(data)); + return (_loadingProfileUserData); + } + + return (profile); + } + + public LightlessProfileData GetLightlessProfile(UserData data) + { + if (!_lightlessProfiles.TryGetValue(data, out var profile)) + { + _logger.LogTrace("Requesting Lightless profile for {data}", data); + _ = Task.Run(() => GetLightlessProfileFromService(data)); + return _loadingProfileData; + } + + return profile; + } + + + /// + /// Fetches Group Profile from cache or API + /// + /// Group Data of given group + /// LightlessGroupProfileData of given group + public LightlessGroupProfileData GetLightlessGroupProfile(GroupData data) + { + if (!_lightlessGroupProfiles.TryGetValue(data, out var profile)) + { + _logger.LogTrace("Requesting group profile for {data}", data); + _ = Task.Run(() => GetLightlessProfileFromService(data)); + return (_loadingProfileGroupData); + } + + return (profile); + } + + /// + /// Fetching the user profile data from the API + /// + /// User you want the profile from + /// New entry in the user profiles to fetch + private async Task GetLightlessProfileFromService(UserData data) + { + try + { + _logger.LogTrace("Inputting loading data in _lightlessUserProfiles for User {data}", data.AliasOrUID); + _lightlessUserProfiles[data] = _loadingProfileUserData; + _lightlessProfiles[data] = _loadingProfileData; + var profile = await _apiController.UserGetProfile(new API.Dto.User.UserDto(data)).ConfigureAwait(false); + var tags = profile.Tags ?? _emptyTagSet; + var profileData = BuildProfileData(data, profile, tags); + var supporterImage = !string.IsNullOrEmpty(data.Alias) && !string.Equals(data.Alias, data.UID, StringComparison.Ordinal) + ? _lightlessSupporter + : string.Empty; + LightlessUserProfileData profileUserData = new( + IsFlagged: profile.Disabled, + IsNSFW: profile.IsNSFW ?? false, + Base64ProfilePicture: string.IsNullOrEmpty(profile.ProfilePictureBase64) ? _lightlessLogo : profile.ProfilePictureBase64, + Base64SupporterPicture: supporterImage, + Base64BannerPicture: string.IsNullOrEmpty(profile.BannerPictureBase64) ? _lightlessBanner : profile.BannerPictureBase64, + Description: string.IsNullOrEmpty(profile.Description) ? _noUserDescription : profile.Description, + Tags: tags); + + _logger.LogTrace("Replacing data in _lightlessUserProfiles for User {data}", data.AliasOrUID); + if (profileUserData.IsNSFW && !_lightlessConfigService.Current.ProfilesAllowNsfw && !string.Equals(_apiController.UID, data.UID, StringComparison.Ordinal)) + { + _lightlessUserProfiles[data] = _nsfwProfileUserData; + _lightlessProfiles[data] = _nsfwProfileData; + } + else + { + _lightlessUserProfiles[data] = profileUserData; + _lightlessProfiles[data] = profileData; + } + } + catch (Exception ex) + { + // if fails save DefaultProfileData to dict + Logger.LogWarning(ex, "Failed to get Profile from service for user {user}", data); + _lightlessUserProfiles[data] = _defaultProfileUserData; + _lightlessProfiles[data] = _defaultProfileData; + } + } + + /// + /// Fetching the group profile data from the API + /// + /// Group you want the profile from + /// New entry in the group profiles to fetch + private async Task GetLightlessProfileFromService(GroupData data) + { + try + { + _logger.LogTrace("Inputting loading data in _lightlessGroupProfiles for Group {data}", data.AliasOrGID); + _lightlessGroupProfiles[data] = _loadingProfileGroupData; + var profile = await _apiController.GroupGetProfile(new API.Dto.Group.GroupDto(data)).ConfigureAwait(false); + var tags = profile.Tags ?? _emptyTagSet; + + LightlessGroupProfileData profileGroupData = new( + IsDisabled: profile.IsDisabled ?? false, + IsNsfw: profile.IsNsfw ?? false, + Base64ProfilePicture: string.IsNullOrEmpty(profile.PictureBase64) ? _lightlessLogo : profile.PictureBase64, + Base64BannerPicture: string.IsNullOrEmpty(profile.BannerBase64) ? _lightlessBanner : profile.BannerBase64, + Description: string.IsNullOrEmpty(profile.Description) ? _noGroupDescription : profile.Description, + Tags: tags); + + _logger.LogTrace("Replacing data in _lightlessGroupProfiles for Group {data}", data.AliasOrGID); + if (profileGroupData.IsNsfw && !_lightlessConfigService.Current.ProfilesAllowNsfw) + { + _lightlessGroupProfiles[data] = _nsfwProfileGroupData; + } + else + { + _lightlessGroupProfiles[data] = profileGroupData; + } + } + catch (Exception ex) + { + // if fails save DefaultProfileData to dict + Logger.LogWarning(ex, "Failed to get Profile from service for syncshell {group}", data); + _lightlessGroupProfiles[data] = _defaultProfileGroupData; + } + } + + private LightlessProfileData BuildProfileData(UserData data, UserProfileDto profile, IReadOnlyList tags) + { + var supporterImage = !string.IsNullOrEmpty(data.Alias) && !string.Equals(data.Alias, data.UID, StringComparison.Ordinal) + ? _lightlessSupporter + : string.Empty; + var profileData = new LightlessProfileData( + IsFlagged: profile.Disabled, + IsNSFW: profile.IsNSFW ?? false, + Base64ProfilePicture: string.IsNullOrEmpty(profile.ProfilePictureBase64) ? _lightlessLogo : profile.ProfilePictureBase64, + Base64SupporterPicture: supporterImage, + Base64BannerPicture: string.IsNullOrEmpty(profile.BannerPictureBase64) ? _lightlessBanner : profile.BannerPictureBase64, + Description: string.IsNullOrEmpty(profile.Description) ? _noDescription : profile.Description, + Tags: tags); + + if (profileData.IsNSFW && !_lightlessConfigService.Current.ProfilesAllowNsfw && !string.Equals(_apiController.UID, data.UID, StringComparison.Ordinal)) + { + return _nsfwProfileData; + } + + return profileData; + } + + public async Task<(UserData User, LightlessProfileData ProfileData)?> GetLightfinderProfileAsync(string hashedCid) + { + if (string.IsNullOrWhiteSpace(hashedCid)) + return null; + + try + { + var profile = await _apiController.UserGetLightfinderProfile(hashedCid).ConfigureAwait(false); + if (profile == null) + return null; + + if (profile.User is null) + { + Logger.LogWarning("Lightfinder profile response missing user info for CID {HashedCid}", hashedCid); + } + + var userData = profile.User ?? new UserData(hashedCid, Alias: "Lightfinder User"); + var profileTags = profile.Tags ?? _emptyTagSet; + var profileData = BuildProfileData(userData, profile, profileTags); + _lightlessProfiles[userData] = profileData; + + return (userData, profileData); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to get Lightfinder profile for CID {HashedCid}", hashedCid); + return null; + } + } +} \ No newline at end of file diff --git a/LightlessSync/Services/Profiles/LightlessUserProfileData.cs b/LightlessSync/Services/Profiles/LightlessUserProfileData.cs new file mode 100644 index 0000000..7e80b10 --- /dev/null +++ b/LightlessSync/Services/Profiles/LightlessUserProfileData.cs @@ -0,0 +1,17 @@ +namespace LightlessSync.Services.Profiles; + +public record LightlessUserProfileData( + bool IsFlagged, + bool IsNSFW, + string Base64ProfilePicture, + string Base64SupporterPicture, + string Base64BannerPicture, + string Description, + IReadOnlyList Tags) +{ + public Lazy ImageData { get; } = new(() => ConvertSafe(Base64ProfilePicture)); + public Lazy SupporterImageData { get; } = new(() => ConvertSafe(Base64SupporterPicture)); + public Lazy BannerImageData { get; } = new(() => ConvertSafe(Base64BannerPicture)); + + private static byte[] ConvertSafe(string value) => string.IsNullOrEmpty(value) ? Array.Empty() : Convert.FromBase64String(value); +} diff --git a/LightlessSync/Services/Rendering/PctDrawListExtensions.cs b/LightlessSync/Services/Rendering/PctDrawListExtensions.cs new file mode 100644 index 0000000..20bcb45 --- /dev/null +++ b/LightlessSync/Services/Rendering/PctDrawListExtensions.cs @@ -0,0 +1,82 @@ +using System.Numerics; +using System.Reflection; +using Dalamud.Bindings.ImGui; +using Pictomancy; + +namespace LightlessSync.Services.Rendering; + +internal static class PctDrawListExtensions +{ + private static readonly FieldInfo? DrawListField = typeof(PctDrawList).GetField("_drawList", BindingFlags.Instance | BindingFlags.NonPublic); + + private static bool TryGetImDrawList(PctDrawList drawList, out ImDrawListPtr ptr) + { + ptr = default; + if (DrawListField == null) + return false; + + if (DrawListField.GetValue(drawList) is ImDrawListPtr list) + { + ptr = list; + return true; + } + + return false; + } + + public static void AddScreenText(this PctDrawList drawList, Vector2 screenPosition, string text, uint color, float fontSize, Vector2? pivot = null, uint? outlineColor = null, ImFontPtr fontOverride = default) + { + if (drawList == null || string.IsNullOrEmpty(text)) + return; + + if (!TryGetImDrawList(drawList, out var imDrawList)) + return; + + var font = fontOverride.IsNull ? ImGui.GetFont() : fontOverride; + if (font.IsNull) + return; + + var size = MathF.Max(1f, fontSize); + var pivotValue = pivot ?? new Vector2(0.5f, 0.5f); + + Vector2 measured; + float calcFontSize; + if (!fontOverride.IsNull) + { + ImGui.PushFont(font); + measured = ImGui.CalcTextSize(text); + calcFontSize = ImGui.GetFontSize(); + ImGui.PopFont(); + } + else + { + measured = ImGui.CalcTextSize(text); + calcFontSize = ImGui.GetFontSize(); + } + + if (calcFontSize > 0f && MathF.Abs(size - calcFontSize) > 0.001f) + { + measured *= size / calcFontSize; + } + + var drawPos = screenPosition - measured * pivotValue; + if (outlineColor.HasValue) + { + var thickness = MathF.Max(1f, size / 24f); + Span offsets = stackalloc Vector2[4] + { + new Vector2(1f, 0f), + new Vector2(-1f, 0f), + new Vector2(0f, 1f), + new Vector2(0f, -1f) + }; + + foreach (var offset in offsets) + { + imDrawList.AddText(font, size, drawPos + offset * thickness, outlineColor.Value, text); + } + } + + imDrawList.AddText(font, size, drawPos, color, text); + } +} diff --git a/LightlessSync/Services/Rendering/PictomancyService.cs b/LightlessSync/Services/Rendering/PictomancyService.cs new file mode 100644 index 0000000..7d12b4c --- /dev/null +++ b/LightlessSync/Services/Rendering/PictomancyService.cs @@ -0,0 +1,47 @@ +using Dalamud.Plugin; +using Microsoft.Extensions.Logging; +using Pictomancy; + +namespace LightlessSync.Services.Rendering; + +public sealed class PictomancyService : IDisposable +{ + private readonly ILogger _logger; + private bool _initialized; + + public PictomancyService(ILogger logger, IDalamudPluginInterface pluginInterface) + { + _logger = logger; + + try + { + PictoService.Initialize(pluginInterface); + _initialized = true; + if (_logger.IsEnabled(LogLevel.Debug)) + _logger.LogDebug("Pictomancy initialized"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to initialize Pictomancy"); + } + } + + public void Dispose() + { + if (!_initialized) + return; + + try + { + PictoService.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to dispose Pictomancy"); + } + finally + { + _initialized = false; + } + } +} diff --git a/LightlessSync/Services/ServerConfiguration/ServerConfigurationManager.cs b/LightlessSync/Services/ServerConfiguration/ServerConfigurationManager.cs index 388ac87..5cb3e15 100644 --- a/LightlessSync/Services/ServerConfiguration/ServerConfigurationManager.cs +++ b/LightlessSync/Services/ServerConfiguration/ServerConfigurationManager.cs @@ -252,9 +252,16 @@ public class ServerConfigurationManager public void SelectServer(int idx) { + var previousIndex = _configService.Current.CurrentServer; _configService.Current.CurrentServer = idx; CurrentServer!.FullPause = false; Save(); + + if (previousIndex != idx) + { + var serverUrl = CurrentServer.ServerUri; + _lightlessMediator.Publish(new ActiveServerChangedMessage(serverUrl)); + } } internal void AddCurrentCharacterToServer(int serverSelectionIndex = -1) diff --git a/LightlessSync/Services/TextureCompression/IndexDownscaler.cs b/LightlessSync/Services/TextureCompression/IndexDownscaler.cs new file mode 100644 index 0000000..615a5e2 --- /dev/null +++ b/LightlessSync/Services/TextureCompression/IndexDownscaler.cs @@ -0,0 +1,312 @@ +using System.Numerics; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +/* + * Index upscaler code (converted/reversed for downscaling purposes) provided by Ny + * thank you!! +*/ + +namespace LightlessSync.Services.TextureCompression; + +internal static class IndexDownscaler +{ + private static readonly Vector2[] SampleOffsets = + { + new(0.25f, 0.25f), + new(0.75f, 0.25f), + new(0.25f, 0.75f), + new(0.75f, 0.75f), + }; + + public static Image Downscale(Image source, int targetWidth, int targetHeight, int blockMultiple) + { + var current = source.Clone(); + + while (current.Width > targetWidth || current.Height > targetHeight) + { + var nextWidth = Math.Max(targetWidth, Math.Max(blockMultiple, current.Width / 2)); + var nextHeight = Math.Max(targetHeight, Math.Max(blockMultiple, current.Height / 2)); + var next = new Image(nextWidth, nextHeight); + + for (var y = 0; y < nextHeight; y++) + { + var srcY = Math.Min(current.Height - 1, y * 2); + for (var x = 0; x < nextWidth; x++) + { + var srcX = Math.Min(current.Width - 1, x * 2); + + var topLeft = current[srcX, srcY]; + var topRight = current[Math.Min(current.Width - 1, srcX + 1), srcY]; + var bottomLeft = current[srcX, Math.Min(current.Height - 1, srcY + 1)]; + var bottomRight = current[Math.Min(current.Width - 1, srcX + 1), Math.Min(current.Height - 1, srcY + 1)]; + + next[x, y] = DownscaleIndexBlock(topLeft, topRight, bottomLeft, bottomRight); + } + } + + current.Dispose(); + current = next; + } + + return current; + } + + private static Rgba32 DownscaleIndexBlock(in Rgba32 topLeft, in Rgba32 topRight, in Rgba32 bottomLeft, in Rgba32 bottomRight) + { + Span ordered = stackalloc Rgba32[4] + { + bottomLeft, + bottomRight, + topRight, + topLeft + }; + + Span weights = stackalloc float[4]; + var hasContribution = false; + + foreach (var sample in SampleOffsets) + { + if (TryAccumulateSampleWeights(ordered, sample, weights)) + { + hasContribution = true; + } + } + + if (hasContribution) + { + var bestIndex = IndexOfMax(weights); + if (bestIndex >= 0 && weights[bestIndex] > 0f) + { + return ordered[bestIndex]; + } + } + + Span fallback = stackalloc Rgba32[4] { topLeft, topRight, bottomLeft, bottomRight }; + return PickMajorityColor(fallback); + } + + private static bool TryAccumulateSampleWeights(ReadOnlySpan colors, in Vector2 sampleUv, Span weights) + { + var red = new Vector4( + colors[0].R / 255f, + colors[1].R / 255f, + colors[2].R / 255f, + colors[3].R / 255f); + + var symbols = QuantizeSymbols(red); + var cellUv = ComputeShiftedUv(sampleUv); + + Span order = stackalloc int[4]; + order[0] = 0; + order[1] = 1; + order[2] = 2; + order[3] = 3; + + ApplySymmetry(ref symbols, ref cellUv, order); + + var equality = BuildEquality(symbols, symbols.W); + var selector = BuildSelector(equality, symbols, cellUv); + + const uint lut = 0x00000C07u; + + if (((lut >> (int)selector) & 1u) != 0u) + { + weights[order[3]] += 1f; + return true; + } + + if (selector == 3u) + { + equality = BuildEquality(symbols, symbols.Z); + } + + var weight = ComputeWeight(equality, cellUv); + if (weight <= 1e-6f) + { + return false; + } + + var factor = 1f / weight; + + var wW = equality.W * (1f - cellUv.X) * (1f - cellUv.Y) * factor; + var wX = equality.X * (1f - cellUv.X) * cellUv.Y * factor; + var wZ = equality.Z * cellUv.X * (1f - cellUv.Y) * factor; + var wY = equality.Y * cellUv.X * cellUv.Y * factor; + + var contributed = false; + + if (wW > 0f) + { + weights[order[3]] += wW; + contributed = true; + } + + if (wX > 0f) + { + weights[order[0]] += wX; + contributed = true; + } + + if (wZ > 0f) + { + weights[order[2]] += wZ; + contributed = true; + } + + if (wY > 0f) + { + weights[order[1]] += wY; + contributed = true; + } + + return contributed; + } + + private static Vector4 QuantizeSymbols(in Vector4 channel) + => new( + Quantize(channel.X), + Quantize(channel.Y), + Quantize(channel.Z), + Quantize(channel.W)); + + private static float Quantize(float value) + { + var clamped = Math.Clamp(value, 0f, 1f); + return (MathF.Round(clamped * 16f) + 0.5f) / 16f; + } + + private static void ApplySymmetry(ref Vector4 symbols, ref Vector2 cellUv, Span order) + { + if (cellUv.X >= 0.5f) + { + symbols = SwapYxwz(symbols, order); + cellUv.X = 1f - cellUv.X; + } + + if (cellUv.Y >= 0.5f) + { + symbols = SwapWzyx(symbols, order); + cellUv.Y = 1f - cellUv.Y; + } + } + + private static Vector4 BuildEquality(in Vector4 symbols, float reference) + => new( + AreEqual(symbols.X, reference) ? 1f : 0f, + AreEqual(symbols.Y, reference) ? 1f : 0f, + AreEqual(symbols.Z, reference) ? 1f : 0f, + AreEqual(symbols.W, reference) ? 1f : 0f); + + private static uint BuildSelector(in Vector4 equality, in Vector4 symbols, in Vector2 cellUv) + { + uint selector = 0; + if (equality.X > 0.5f) selector |= 4u; + if (equality.Y > 0.5f) selector |= 8u; + if (equality.Z > 0.5f) selector |= 16u; + if (AreEqual(symbols.X, symbols.Z)) selector |= 2u; + if (cellUv.X + cellUv.Y >= 0.5f) selector |= 1u; + + return selector; + } + + private static float ComputeWeight(in Vector4 equality, in Vector2 cellUv) + => equality.W * (1f - cellUv.X) * (1f - cellUv.Y) + + equality.X * (1f - cellUv.X) * cellUv.Y + + equality.Z * cellUv.X * (1f - cellUv.Y) + + equality.Y * cellUv.X * cellUv.Y; + + private static Vector2 ComputeShiftedUv(in Vector2 uv) + { + var shifted = new Vector2( + uv.X - MathF.Floor(uv.X), + uv.Y - MathF.Floor(uv.Y)); + + shifted.X -= 0.5f; + if (shifted.X < 0f) + { + shifted.X += 1f; + } + + shifted.Y -= 0.5f; + if (shifted.Y < 0f) + { + shifted.Y += 1f; + } + + return shifted; + } + + private static Vector4 SwapYxwz(in Vector4 v, Span order) + { + var o0 = order[0]; + var o1 = order[1]; + var o2 = order[2]; + var o3 = order[3]; + + order[0] = o1; + order[1] = o0; + order[2] = o3; + order[3] = o2; + + return new Vector4(v.Y, v.X, v.W, v.Z); + } + + private static Vector4 SwapWzyx(in Vector4 v, Span order) + { + var o0 = order[0]; + var o1 = order[1]; + var o2 = order[2]; + var o3 = order[3]; + + order[0] = o3; + order[1] = o2; + order[2] = o1; + order[3] = o0; + + return new Vector4(v.W, v.Z, v.Y, v.X); + } + + private static int IndexOfMax(ReadOnlySpan values) + { + var bestIndex = -1; + var bestValue = 0f; + + for (var i = 0; i < values.Length; i++) + { + if (values[i] > bestValue) + { + bestValue = values[i]; + bestIndex = i; + } + } + + return bestIndex; + } + + private static bool AreEqual(float a, float b) => MathF.Abs(a - b) <= 1e-5f; + + private static Rgba32 PickMajorityColor(ReadOnlySpan colors) + { + var counts = new Dictionary(colors.Length); + foreach (var color in colors) + { + if (counts.TryGetValue(color, out var count)) + { + counts[color] = count + 1; + } + else + { + counts[color] = 1; + } + } + + return counts + .OrderByDescending(kvp => kvp.Value) + .ThenByDescending(kvp => kvp.Key.A) + .ThenByDescending(kvp => kvp.Key.R) + .ThenByDescending(kvp => kvp.Key.G) + .ThenByDescending(kvp => kvp.Key.B) + .First().Key; + } +} \ No newline at end of file diff --git a/LightlessSync/Services/TextureCompression/TexFileHelper.cs b/LightlessSync/Services/TextureCompression/TexFileHelper.cs new file mode 100644 index 0000000..7258fbf --- /dev/null +++ b/LightlessSync/Services/TextureCompression/TexFileHelper.cs @@ -0,0 +1,280 @@ +using System.Runtime.InteropServices; +using Lumina.Data.Files; +using OtterTex; + +namespace LightlessSync.Services.TextureCompression; + +// base taken from penumbra mostly +internal static class TexFileHelper +{ + private const int HeaderSize = 80; + private const int MaxMipLevels = 13; + + public static ScratchImage Load(string path) + { + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + return Load(stream); + } + + public static ScratchImage Load(Stream stream) + { + using var reader = new BinaryReader(stream, System.Text.Encoding.UTF8, leaveOpen: true); + var header = ReadHeader(reader); + var meta = CreateMeta(header); + meta.MipLevels = ComputeMipCount(stream.Length, header, meta); + if (meta.MipLevels == 0) + { + throw new InvalidOperationException("TEX file does not contain a valid mip chain."); + } + + var scratch = ScratchImage.Initialize(meta); + ReadPixelData(reader, scratch); + return scratch; + } + + public static void Save(string path, ScratchImage image) + { + var header = BuildHeader(image); + if (header.Format == TexFile.TextureFormat.Unknown) + { + throw new InvalidOperationException($"Unable to export TEX file with unsupported format {image.Meta.Format}."); + } + + var mode = File.Exists(path) ? FileMode.Truncate : FileMode.CreateNew; + using var stream = new FileStream(path, mode, FileAccess.Write, FileShare.Read); + using var writer = new BinaryWriter(stream); + WriteHeader(writer, header); + writer.Write(image.Pixels); + GC.KeepAlive(image); + } + + private static TexFile.TexHeader ReadHeader(BinaryReader reader) + { + Span buffer = stackalloc byte[HeaderSize]; + var read = reader.Read(buffer); + if (read != HeaderSize) + { + throw new EndOfStreamException($"Incomplete TEX header: expected {HeaderSize} bytes, read {read} bytes."); + } + + return MemoryMarshal.Read(buffer); + } + + private static TexMeta CreateMeta(in TexFile.TexHeader header) + { + var meta = new TexMeta + { + Width = header.Width, + Height = header.Height, + Depth = Math.Max(header.Depth, (ushort)1), + ArraySize = 1, + MipLevels = header.MipCount, + Format = header.Format.ToDxgi(), + Dimension = header.Type.ToDimension(), + MiscFlags = header.Type.HasFlag(TexFile.Attribute.TextureTypeCube) ? D3DResourceMiscFlags.TextureCube : 0, + MiscFlags2 = 0, + }; + + if (meta.Format == DXGIFormat.Unknown) + { + throw new InvalidOperationException($"TEX format {header.Format} cannot be mapped to DXGI."); + } + + if (meta.Dimension == TexDimension.Unknown) + { + throw new InvalidOperationException($"Unrecognised TEX dimension attribute {header.Type}."); + } + + return meta; + } + + private static unsafe int ComputeMipCount(long totalLength, in TexFile.TexHeader header, in TexMeta meta) + { + var width = Math.Max(meta.Width, 1); + var height = Math.Max(meta.Height, 1); + var minSide = meta.Format.IsCompressed() ? 4 : 1; + var bitsPerPixel = meta.Format.BitsPerPixel(); + + var expectedOffset = HeaderSize; + var remaining = totalLength - HeaderSize; + + for (var level = 0; level < MaxMipLevels; level++) + { + var declaredOffset = header.OffsetToSurface[level]; + if (declaredOffset == 0) + { + return level; + } + + if (declaredOffset != expectedOffset || remaining <= 0) + { + return level; + } + + var mipSize = (int)((long)width * height * bitsPerPixel / 8); + if (mipSize > remaining) + { + return level; + } + + expectedOffset += mipSize; + remaining -= mipSize; + + if (width <= minSide && height <= minSide) + { + return level + 1; + } + + width = Math.Max(width / 2, minSide); + height = Math.Max(height / 2, minSide); + } + + return MaxMipLevels; + } + + private static unsafe void ReadPixelData(BinaryReader reader, ScratchImage image) + { + fixed (byte* destination = image.Pixels) + { + var span = new Span(destination, image.Pixels.Length); + var read = reader.Read(span); + if (read < span.Length) + { + throw new InvalidDataException($"TEX pixel buffer is truncated (read {read} of {span.Length} bytes)."); + } + } + } + + private static TexFile.TexHeader BuildHeader(ScratchImage image) + { + var meta = image.Meta; + var header = new TexFile.TexHeader + { + Width = (ushort)meta.Width, + Height = (ushort)meta.Height, + Depth = (ushort)Math.Max(meta.Depth, 1), + MipCount = (byte)Math.Min(meta.MipLevels, MaxMipLevels), + Format = meta.Format.ToTex(), + Type = meta.Dimension switch + { + _ when meta.IsCubeMap => TexFile.Attribute.TextureTypeCube, + TexDimension.Tex1D => TexFile.Attribute.TextureType1D, + TexDimension.Tex2D => TexFile.Attribute.TextureType2D, + TexDimension.Tex3D => TexFile.Attribute.TextureType3D, + _ => 0, + }, + }; + + PopulateOffsets(ref header, image); + return header; + } + + private static unsafe void PopulateOffsets(ref TexFile.TexHeader header, ScratchImage image) + { + var index = 0; + fixed (byte* basePtr = image.Pixels) + { + foreach (var mip in image.Images) + { + if (index >= MaxMipLevels) + { + break; + } + + var byteOffset = (byte*)mip.Pixels - basePtr; + header.OffsetToSurface[index++] = HeaderSize + (uint)byteOffset; + } + } + + while (index < MaxMipLevels) + { + header.OffsetToSurface[index++] = 0; + } + + header.LodOffset[0] = 0; + header.LodOffset[1] = (byte)Math.Min(header.MipCount - 1, 1); + header.LodOffset[2] = (byte)Math.Min(header.MipCount - 1, 2); + } + + private static unsafe void WriteHeader(BinaryWriter writer, in TexFile.TexHeader header) + { + writer.Write((uint)header.Type); + writer.Write((uint)header.Format); + writer.Write(header.Width); + writer.Write(header.Height); + writer.Write(header.Depth); + writer.Write((byte)(header.MipCount | (header.MipUnknownFlag ? 0x80 : 0))); + writer.Write(header.ArraySize); + writer.Write(header.LodOffset[0]); + writer.Write(header.LodOffset[1]); + writer.Write(header.LodOffset[2]); + for (var i = 0; i < MaxMipLevels; i++) + { + writer.Write(header.OffsetToSurface[i]); + } + } + + private static TexDimension ToDimension(this TexFile.Attribute attribute) + => (attribute & TexFile.Attribute.TextureTypeMask) switch + { + TexFile.Attribute.TextureType1D => TexDimension.Tex1D, + TexFile.Attribute.TextureType2D => TexDimension.Tex2D, + TexFile.Attribute.TextureType3D => TexDimension.Tex3D, + _ => TexDimension.Unknown, + }; + + private static DXGIFormat ToDxgi(this TexFile.TextureFormat format) + => format switch + { + TexFile.TextureFormat.L8 => DXGIFormat.R8UNorm, + TexFile.TextureFormat.A8 => DXGIFormat.A8UNorm, + TexFile.TextureFormat.B4G4R4A4 => DXGIFormat.B4G4R4A4UNorm, + TexFile.TextureFormat.B5G5R5A1 => DXGIFormat.B5G5R5A1UNorm, + TexFile.TextureFormat.B8G8R8A8 => DXGIFormat.B8G8R8A8UNorm, + TexFile.TextureFormat.B8G8R8X8 => DXGIFormat.B8G8R8X8UNorm, + TexFile.TextureFormat.R32F => DXGIFormat.R32Float, + TexFile.TextureFormat.R16G16F => DXGIFormat.R16G16Float, + TexFile.TextureFormat.R32G32F => DXGIFormat.R32G32Float, + TexFile.TextureFormat.R16G16B16A16F => DXGIFormat.R16G16B16A16Float, + TexFile.TextureFormat.R32G32B32A32F => DXGIFormat.R32G32B32A32Float, + TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm, + TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm, + TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, + (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, + TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, + (TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HSF16, + TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, + TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless, + TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless, + TexFile.TextureFormat.Shadow16 => DXGIFormat.R16Typeless, + TexFile.TextureFormat.Shadow24 => DXGIFormat.R24G8Typeless, + _ => DXGIFormat.Unknown, + }; + + private static TexFile.TextureFormat ToTex(this DXGIFormat format) + => format switch + { + DXGIFormat.R8UNorm => TexFile.TextureFormat.L8, + DXGIFormat.A8UNorm => TexFile.TextureFormat.A8, + DXGIFormat.B4G4R4A4UNorm => TexFile.TextureFormat.B4G4R4A4, + DXGIFormat.B5G5R5A1UNorm => TexFile.TextureFormat.B5G5R5A1, + DXGIFormat.B8G8R8A8UNorm => TexFile.TextureFormat.B8G8R8A8, + DXGIFormat.B8G8R8X8UNorm => TexFile.TextureFormat.B8G8R8X8, + DXGIFormat.R32Float => TexFile.TextureFormat.R32F, + DXGIFormat.R16G16Float => TexFile.TextureFormat.R16G16F, + DXGIFormat.R32G32Float => TexFile.TextureFormat.R32G32F, + DXGIFormat.R16G16B16A16Float => TexFile.TextureFormat.R16G16B16A16F, + DXGIFormat.R32G32B32A32Float => TexFile.TextureFormat.R32G32B32A32F, + DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1, + DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2, + DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3, + DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, + DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5, + DXGIFormat.BC6HSF16 => (TexFile.TextureFormat)0x6330, + DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7, + DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16, + DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8, + DXGIFormat.R16Typeless => TexFile.TextureFormat.Shadow16, + _ => TexFile.TextureFormat.Unknown, + }; +} diff --git a/LightlessSync/Services/TextureCompression/TextureCompressionCapabilities.cs b/LightlessSync/Services/TextureCompression/TextureCompressionCapabilities.cs new file mode 100644 index 0000000..bba27cd --- /dev/null +++ b/LightlessSync/Services/TextureCompression/TextureCompressionCapabilities.cs @@ -0,0 +1,61 @@ +using System.Collections.Immutable; +using Penumbra.Api.Enums; + +namespace LightlessSync.Services.TextureCompression; + +internal static class TextureCompressionCapabilities +{ + private static readonly ImmutableDictionary TexTargets = + new Dictionary + { + [TextureCompressionTarget.BC1] = TextureType.Bc1Tex, + [TextureCompressionTarget.BC3] = TextureType.Bc3Tex, + [TextureCompressionTarget.BC4] = TextureType.Bc4Tex, + [TextureCompressionTarget.BC5] = TextureType.Bc5Tex, + [TextureCompressionTarget.BC7] = TextureType.Bc7Tex, + }.ToImmutableDictionary(); + + private static readonly ImmutableDictionary DdsTargets = + new Dictionary + { + [TextureCompressionTarget.BC1] = TextureType.Bc1Dds, + [TextureCompressionTarget.BC3] = TextureType.Bc3Dds, + [TextureCompressionTarget.BC4] = TextureType.Bc4Dds, + [TextureCompressionTarget.BC5] = TextureType.Bc5Dds, + [TextureCompressionTarget.BC7] = TextureType.Bc7Dds, + }.ToImmutableDictionary(); + + private static readonly TextureCompressionTarget[] SelectableTargetsCache = TexTargets + .Select(kvp => kvp.Key) + .OrderBy(t => t) + .ToArray(); + + private static readonly HashSet SelectableTargetSet = SelectableTargetsCache.ToHashSet(); + + public static IReadOnlyList SelectableTargets => SelectableTargetsCache; + + public static TextureCompressionTarget DefaultTarget => TextureCompressionTarget.BC7; + + public static bool IsSelectable(TextureCompressionTarget target) => SelectableTargetSet.Contains(target); + + public static TextureCompressionTarget Normalize(TextureCompressionTarget? desired) + { + if (desired.HasValue && IsSelectable(desired.Value)) + { + return desired.Value; + } + + return DefaultTarget; + } + + public static bool TryGetPenumbraTarget(TextureCompressionTarget target, string? outputPath, out TextureType textureType) + { + if (!string.IsNullOrWhiteSpace(outputPath) && + string.Equals(Path.GetExtension(outputPath), ".dds", StringComparison.OrdinalIgnoreCase)) + { + return DdsTargets.TryGetValue(target, out textureType); + } + + return TexTargets.TryGetValue(target, out textureType); + } +} diff --git a/LightlessSync/Services/TextureCompression/TextureCompressionRequest.cs b/LightlessSync/Services/TextureCompression/TextureCompressionRequest.cs new file mode 100644 index 0000000..18681d8 --- /dev/null +++ b/LightlessSync/Services/TextureCompression/TextureCompressionRequest.cs @@ -0,0 +1,7 @@ + +namespace LightlessSync.Services.TextureCompression; + +public sealed record TextureCompressionRequest( + string PrimaryFilePath, + IReadOnlyList DuplicateFilePaths, + TextureCompressionTarget Target); diff --git a/LightlessSync/Services/TextureCompression/TextureCompressionService.cs b/LightlessSync/Services/TextureCompression/TextureCompressionService.cs new file mode 100644 index 0000000..c31539f --- /dev/null +++ b/LightlessSync/Services/TextureCompression/TextureCompressionService.cs @@ -0,0 +1,325 @@ +using LightlessSync.Interop.Ipc; +using LightlessSync.FileCache; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Enums; + +namespace LightlessSync.Services.TextureCompression; + +public sealed class TextureCompressionService +{ + private readonly ILogger _logger; + private readonly IpcManager _ipcManager; + private readonly FileCacheManager _fileCacheManager; + + public IReadOnlyList SelectableTargets => TextureCompressionCapabilities.SelectableTargets; + public TextureCompressionTarget DefaultTarget => TextureCompressionCapabilities.DefaultTarget; + + public TextureCompressionService( + ILogger logger, + IpcManager ipcManager, + FileCacheManager fileCacheManager) + { + _logger = logger; + _ipcManager = ipcManager; + _fileCacheManager = fileCacheManager; + } + + public async Task ConvertTexturesAsync( + IReadOnlyList requests, + IProgress? progress, + CancellationToken token) + { + if (requests.Count == 0) + { + return; + } + + var total = requests.Count; + var completed = 0; + + foreach (var request in requests) + { + token.ThrowIfCancellationRequested(); + + if (!TextureCompressionCapabilities.TryGetPenumbraTarget(request.Target, request.PrimaryFilePath, out var textureType)) + { + _logger.LogWarning("Unsupported compression target {Target} requested.", request.Target); + completed++; + continue; + } + + await RunPenumbraConversionAsync(request, textureType, total, completed, progress, token).ConfigureAwait(false); + + completed++; + } + } + + public bool IsTargetSelectable(TextureCompressionTarget target) => TextureCompressionCapabilities.IsSelectable(target); + + public TextureCompressionTarget NormalizeTarget(TextureCompressionTarget? desired) => + TextureCompressionCapabilities.Normalize(desired); + + private async Task RunPenumbraConversionAsync( + TextureCompressionRequest request, + TextureType targetType, + int total, + int completedBefore, + IProgress? progress, + CancellationToken token) + { + var primaryPath = request.PrimaryFilePath; + var displayJob = new TextureConversionJob( + primaryPath, + primaryPath, + targetType, + IncludeMipMaps: true, + request.DuplicateFilePaths); + + var backupPath = CreateBackupCopy(primaryPath); + var conversionJob = displayJob with { InputFile = backupPath }; + + progress?.Report(new TextureConversionProgress(completedBefore, total, displayJob)); + + try + { + WaitForAccess(primaryPath); + await _ipcManager.Penumbra.ConvertTextureFiles(_logger, new[] { conversionJob }, null, token).ConfigureAwait(false); + + if (!IsValidConversionResult(displayJob.OutputFile)) + { + throw new InvalidOperationException($"Penumbra conversion produced no output for {displayJob.OutputFile}."); + } + + UpdateFileCache(displayJob); + + progress?.Report(new TextureConversionProgress(completedBefore + 1, total, displayJob)); + } + catch (Exception ex) + { + RestoreFromBackup(backupPath, displayJob.OutputFile, displayJob.DuplicateTargets, ex); + throw; + } + finally + { + CleanupBackup(backupPath); + } + } + + private void UpdateFileCache(TextureConversionJob job) + { + var paths = new HashSet(StringComparer.OrdinalIgnoreCase) + { + job.OutputFile + }; + + if (job.DuplicateTargets is { Count: > 0 }) + { + foreach (var duplicate in job.DuplicateTargets) + { + paths.Add(duplicate); + } + } + + if (paths.Count == 0) + { + return; + } + + var cacheEntries = _fileCacheManager.GetFileCachesByPaths(paths.ToArray()); + foreach (var path in paths) + { + if (!cacheEntries.TryGetValue(path, out var entry) || entry is null) + { + entry = _fileCacheManager.CreateFileEntry(path); + if (entry is null) + { + _logger.LogWarning("Unable to locate cache entry for {Path}; skipping hash refresh", path); + continue; + } + } + + try + { + _fileCacheManager.UpdateHashedFile(entry); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to refresh file cache entry for {Path}", path); + } + } + } + + private static readonly string WorkingDirectory = + Path.Combine(Path.GetTempPath(), "LightlessSync.TextureCompression"); + + private static string CreateBackupCopy(string filePath) + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"Cannot back up missing texture file {filePath}.", filePath); + } + + Directory.CreateDirectory(WorkingDirectory); + + var extension = Path.GetExtension(filePath); + if (string.IsNullOrEmpty(extension)) + { + extension = ".tmp"; + } + + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath); + var backupName = $"{fileNameWithoutExtension}.backup.{Guid.NewGuid():N}{extension}"; + var backupPath = Path.Combine(WorkingDirectory, backupName); + + WaitForAccess(filePath); + + File.Copy(filePath, backupPath, overwrite: false); + + return backupPath; + } + + private const int MaxAccessRetries = 10; + private static readonly TimeSpan AccessRetryDelay = TimeSpan.FromMilliseconds(200); + + private static void WaitForAccess(string filePath) + { + if (!File.Exists(filePath)) + { + return; + } + + try + { + File.SetAttributes(filePath, FileAttributes.Normal); + } + catch + { + // ignore attribute changes here + } + + Exception? lastException = null; + for (var attempt = 0; attempt < MaxAccessRetries; attempt++) + { + try + { + using var stream = new FileStream( + filePath, + FileMode.Open, + FileAccess.Read, + FileShare.None); + return; + } + catch (IOException ex) when (IsSharingViolation(ex)) + { + lastException = ex; + } + + Thread.Sleep(AccessRetryDelay); + } + + if (lastException != null) + { + throw lastException; + } + } + + private static bool IsSharingViolation(IOException ex) => + ex.HResult == unchecked((int)0x80070020); + + private void RestoreFromBackup( + string backupPath, + string destinationPath, + IReadOnlyList? duplicateTargets, + Exception reason) + { + if (string.IsNullOrEmpty(backupPath)) + { + _logger.LogWarning(reason, "Conversion failed for {File}, but no backup was available to restore.", destinationPath); + return; + } + + if (!File.Exists(backupPath)) + { + _logger.LogWarning(reason, "Conversion failed for {File}, but backup path {Backup} no longer exists.", destinationPath, backupPath); + return; + } + + try + { + TryReplaceFile(backupPath, destinationPath); + } + catch (Exception restoreEx) + { + _logger.LogError(restoreEx, "Failed to restore texture {File} after conversion failure.", destinationPath); + return; + } + + if (duplicateTargets is { Count: > 0 }) + { + foreach (var duplicate in duplicateTargets) + { + if (string.Equals(destinationPath, duplicate, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + try + { + File.Copy(destinationPath, duplicate, overwrite: true); + } + catch (Exception duplicateEx) + { + _logger.LogDebug(duplicateEx, "Failed to restore duplicate {Duplicate} after conversion failure.", duplicate); + } + } + } + + _logger.LogWarning(reason, "Restored original texture {File} after conversion failure.", destinationPath); + } + + private static void TryReplaceFile(string sourcePath, string destinationPath) + { + WaitForAccess(destinationPath); + + var destinationDirectory = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrEmpty(destinationDirectory)) + { + Directory.CreateDirectory(destinationDirectory); + } + + File.Copy(sourcePath, destinationPath, overwrite: true); + } + + private static void CleanupBackup(string backupPath) + { + if (string.IsNullOrEmpty(backupPath)) + { + return; + } + + try + { + if (File.Exists(backupPath)) + { + File.Delete(backupPath); + } + } + catch + { + // avoid killing successful conversions on cleanup failure + } + } + + private static bool IsValidConversionResult(string path) + { + try + { + var fileInfo = new FileInfo(path); + return fileInfo.Exists && fileInfo.Length > 0; + } + catch + { + return false; + } + } +} diff --git a/LightlessSync/Services/TextureCompression/TextureCompressionTarget.cs b/LightlessSync/Services/TextureCompression/TextureCompressionTarget.cs new file mode 100644 index 0000000..0928da4 --- /dev/null +++ b/LightlessSync/Services/TextureCompression/TextureCompressionTarget.cs @@ -0,0 +1,10 @@ +namespace LightlessSync.Services.TextureCompression; + +public enum TextureCompressionTarget +{ + BC1, + BC3, + BC4, + BC5, + BC7 +} diff --git a/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs b/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs new file mode 100644 index 0000000..7a09ae7 --- /dev/null +++ b/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs @@ -0,0 +1,714 @@ +using System.Collections.Concurrent; +using System.Buffers; +using System.Buffers.Binary; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using OtterTex; +using OtterImage = OtterTex.Image; +using LightlessSync.LightlessConfiguration; +using LightlessSync.FileCache; +using Microsoft.Extensions.Logging; +using Lumina.Data.Files; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +/* + * OtterTex made by Ottermandias + * thank you!! +*/ + +namespace LightlessSync.Services.TextureCompression; + +public sealed class TextureDownscaleService +{ + private const int DefaultTargetMaxDimension = 2048; + private const int MaxSupportedTargetDimension = 8192; + private const int BlockMultiple = 4; + + private readonly ILogger _logger; + private readonly LightlessConfigService _configService; + private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; + private readonly FileCacheManager _fileCacheManager; + + private readonly ConcurrentDictionary _activeJobs = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _downscaledPaths = new(StringComparer.OrdinalIgnoreCase); + private readonly SemaphoreSlim _downscaleSemaphore = new(4); + private static readonly IReadOnlyDictionary BlockCompressedFormatMap = + new Dictionary + { + [70] = TextureCompressionTarget.BC1, // DXGI_FORMAT_BC1_TYPELESS + [71] = TextureCompressionTarget.BC1, // DXGI_FORMAT_BC1_UNORM + [72] = TextureCompressionTarget.BC1, // DXGI_FORMAT_BC1_UNORM_SRGB + + [73] = TextureCompressionTarget.BC3, // DXGI_FORMAT_BC2_TYPELESS + [74] = TextureCompressionTarget.BC3, // DXGI_FORMAT_BC2_UNORM + [75] = TextureCompressionTarget.BC3, // DXGI_FORMAT_BC2_UNORM_SRGB + [76] = TextureCompressionTarget.BC3, // DXGI_FORMAT_BC3_TYPELESS + [77] = TextureCompressionTarget.BC3, // DXGI_FORMAT_BC3_UNORM + [78] = TextureCompressionTarget.BC3, // DXGI_FORMAT_BC3_UNORM_SRGB + + [79] = TextureCompressionTarget.BC4, // DXGI_FORMAT_BC4_TYPELESS + [80] = TextureCompressionTarget.BC4, // DXGI_FORMAT_BC4_UNORM + [81] = TextureCompressionTarget.BC4, // DXGI_FORMAT_BC4_SNORM + + [82] = TextureCompressionTarget.BC5, // DXGI_FORMAT_BC5_TYPELESS + [83] = TextureCompressionTarget.BC5, // DXGI_FORMAT_BC5_UNORM + [84] = TextureCompressionTarget.BC5, // DXGI_FORMAT_BC5_SNORM + + [94] = TextureCompressionTarget.BC7, // DXGI_FORMAT_BC6H_TYPELESS (treated as BC7 for block detection) + [95] = TextureCompressionTarget.BC7, // DXGI_FORMAT_BC6H_UF16 + [96] = TextureCompressionTarget.BC7, // DXGI_FORMAT_BC6H_SF16 + [97] = TextureCompressionTarget.BC7, // DXGI_FORMAT_BC7_TYPELESS + [98] = TextureCompressionTarget.BC7, // DXGI_FORMAT_BC7_UNORM + [99] = TextureCompressionTarget.BC7, // DXGI_FORMAT_BC7_UNORM_SRGB + }; + + public TextureDownscaleService( + ILogger logger, + LightlessConfigService configService, + PlayerPerformanceConfigService playerPerformanceConfigService, + FileCacheManager fileCacheManager) + { + _logger = logger; + _configService = configService; + _playerPerformanceConfigService = playerPerformanceConfigService; + _fileCacheManager = fileCacheManager; + } + + public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind) + { + if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return; + if (_activeJobs.ContainsKey(hash)) return; + + _activeJobs[hash] = Task.Run(async () => + { + await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false); + }, CancellationToken.None); + } + + public string GetPreferredPath(string hash, string originalPath) + { + if (_downscaledPaths.TryGetValue(hash, out var existing) && File.Exists(existing)) + { + return existing; + } + + var resolved = GetExistingDownscaledPath(hash); + if (!string.IsNullOrEmpty(resolved)) + { + _downscaledPaths[hash] = resolved; + return resolved; + } + + return originalPath; + } + + public Task WaitForPendingJobsAsync(IEnumerable? hashes, CancellationToken token) + { + if (hashes is null) + { + return Task.CompletedTask; + } + + var pending = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var hash in hashes) + { + if (string.IsNullOrEmpty(hash) || !seen.Add(hash)) + { + continue; + } + + if (_activeJobs.TryGetValue(hash, out var job)) + { + pending.Add(job); + } + } + + if (pending.Count == 0) + { + return Task.CompletedTask; + } + + return Task.WhenAll(pending).WaitAsync(token); + } + + private async Task DownscaleInternalAsync(string hash, string sourcePath, TextureMapKind mapKind) + { + TexHeaderInfo? headerInfo = null; + string? destination = null; + int targetMaxDimension = 0; + bool onlyDownscaleUncompressed = false; + bool? isIndexTexture = null; + + await _downscaleSemaphore.WaitAsync().ConfigureAwait(false); + try + { + if (!File.Exists(sourcePath)) + { + _logger.LogWarning("Cannot downscale texture {Hash}; source path missing: {Path}", hash, sourcePath); + return; + } + + headerInfo = TryReadTexHeader(sourcePath, out var header) + ? header + : (TexHeaderInfo?)null; + var performanceConfig = _playerPerformanceConfigService.Current; + targetMaxDimension = ResolveTargetMaxDimension(); + onlyDownscaleUncompressed = performanceConfig.OnlyDownscaleUncompressedTextures; + + destination = Path.Combine(GetDownscaledDirectory(), $"{hash}.tex"); + if (File.Exists(destination)) + { + RegisterDownscaledTexture(hash, sourcePath, destination); + return; + } + + var indexTexture = IsIndexMap(mapKind); + isIndexTexture = indexTexture; + if (!indexTexture) + { + if (performanceConfig.EnableNonIndexTextureMipTrim + && await TryDropTopMipAsync(hash, sourcePath, destination, targetMaxDimension, onlyDownscaleUncompressed, headerInfo).ConfigureAwait(false)) + { + return; + } + + if (!performanceConfig.EnableNonIndexTextureMipTrim) + { + _logger.LogTrace("Skipping mip trim for non-index texture {Hash}; feature disabled.", hash); + } + + _downscaledPaths[hash] = sourcePath; + _logger.LogTrace("Skipping downscale for non-index texture {Hash}; no mip reduction required.", hash); + return; + } + + if (!performanceConfig.EnableIndexTextureDownscale) + { + _downscaledPaths[hash] = sourcePath; + _logger.LogTrace("Skipping downscale for index texture {Hash}; feature disabled.", hash); + return; + } + + if (headerInfo is { } headerValue && + headerValue.Width <= targetMaxDimension && + headerValue.Height <= targetMaxDimension) + { + _downscaledPaths[hash] = sourcePath; + _logger.LogTrace("Skipping downscale for index texture {Hash}; header dimensions {Width}x{Height} within target.", hash, headerValue.Width, headerValue.Height); + return; + } + + if (onlyDownscaleUncompressed && headerInfo.HasValue && IsBlockCompressedFormat(headerInfo.Value.Format)) + { + _downscaledPaths[hash] = sourcePath; + _logger.LogTrace("Skipping downscale for index texture {Hash}; block compressed format {Format}.", hash, headerInfo.Value.Format); + return; + } + + using var sourceScratch = TexFileHelper.Load(sourcePath); + using var rgbaScratch = sourceScratch.GetRGBA(out var rgbaInfo).ThrowIfError(rgbaInfo); + + var bytesPerPixel = rgbaInfo.Meta.Format.BitsPerPixel() / 8; + var width = rgbaInfo.Meta.Width; + var height = rgbaInfo.Meta.Height; + var requiredLength = width * height * bytesPerPixel; + + var rgbaPixels = rgbaScratch.Pixels.Slice(0, requiredLength); + using var originalImage = SixLabors.ImageSharp.Image.LoadPixelData(rgbaPixels, width, height); + + var targetSize = CalculateTargetSize(originalImage.Width, originalImage.Height, targetMaxDimension); + if (targetSize.width == originalImage.Width && targetSize.height == originalImage.Height) + { + _downscaledPaths[hash] = sourcePath; + _logger.LogTrace("Skipping downscale for index texture {Hash}; already within bounds.", hash); + return; + } + + using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple); + + using var resizedScratch = CreateScratchImage(resized, targetSize.width, targetSize.height); + using var finalScratch = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm); + + TexFileHelper.Save(destination, finalScratch); + RegisterDownscaledTexture(hash, sourcePath, destination); + } + catch (Exception ex) + { + TryDelete(destination); + _logger.LogWarning( + ex, + "Texture downscale failed for {Hash} ({MapKind}) from {SourcePath} -> {Destination}. TargetMax={TargetMaxDimension}, OnlyUncompressed={OnlyDownscaleUncompressed}, IsIndex={IsIndexTexture}, HeaderFormat={HeaderFormat}", + hash, + mapKind, + sourcePath, + destination ?? "", + targetMaxDimension, + onlyDownscaleUncompressed, + isIndexTexture, + headerInfo?.Format); + } + finally + { + _downscaleSemaphore.Release(); + _activeJobs.TryRemove(hash, out _); + } + } + + private static (int width, int height) CalculateTargetSize(int width, int height, int targetMaxDimension) + { + var resultWidth = width; + var resultHeight = height; + + while (Math.Max(resultWidth, resultHeight) > targetMaxDimension) + { + resultWidth = Math.Max(BlockMultiple, resultWidth / 2); + resultHeight = Math.Max(BlockMultiple, resultHeight / 2); + } + + return (resultWidth, resultHeight); + } + + private static ScratchImage CreateScratchImage(Image image, int width, int height) + { + const int BytesPerPixel = 4; + var requiredLength = width * height * BytesPerPixel; + + static ScratchImage Create(ReadOnlySpan pixels, int width, int height) + { + var scratchResult = ScratchImage.FromRGBA(pixels, width, height, out var creationInfo); + return scratchResult.ThrowIfError(creationInfo); + } + + if (image.DangerousTryGetSinglePixelMemory(out var pixelMemory)) + { + var byteSpan = MemoryMarshal.AsBytes(pixelMemory.Span); + if (byteSpan.Length < requiredLength) + { + throw new InvalidOperationException($"Image buffer shorter than expected ({byteSpan.Length} < {requiredLength})."); + } + + return Create(byteSpan.Slice(0, requiredLength), width, height); + } + + var rented = ArrayPool.Shared.Rent(requiredLength); + try + { + var rentedSpan = rented.AsSpan(0, requiredLength); + image.CopyPixelDataTo(rentedSpan); + return Create(rentedSpan, width, height); + } + finally + { + ArrayPool.Shared.Return(rented); + } + } + + private static bool IsIndexMap(TextureMapKind kind) + => kind is TextureMapKind.Mask + or TextureMapKind.Index; + + private Task TryDropTopMipAsync( + string hash, + string sourcePath, + string destination, + int targetMaxDimension, + bool onlyDownscaleUncompressed, + TexHeaderInfo? headerInfo = null) + { + TexHeaderInfo? header = headerInfo; + int dropCount = -1; + int originalWidth = 0; + int originalHeight = 0; + int originalMipLevels = 0; + + try + { + if (!File.Exists(sourcePath)) + { + _logger.LogWarning("Cannot trim mip levels for texture {Hash}; source path missing: {Path}", hash, sourcePath); + return Task.FromResult(false); + } + + if (header is null && TryReadTexHeader(sourcePath, out var discoveredHeader)) + { + header = discoveredHeader; + } + + if (header is TexHeaderInfo info) + { + if (onlyDownscaleUncompressed && IsBlockCompressedFormat(info.Format)) + { + _logger.LogTrace("Skipping mip trim for texture {Hash}; block compressed format {Format}.", hash, info.Format); + return Task.FromResult(false); + } + + if (info.MipLevels <= 1) + { + return Task.FromResult(false); + } + + var headerDepth = info.Depth == 0 ? 1 : info.Depth; + if (!ShouldTrimDimensions(info.Width, info.Height, headerDepth, targetMaxDimension)) + { + return Task.FromResult(false); + } + } + + using var original = TexFileHelper.Load(sourcePath); + var meta = original.Meta; + originalWidth = meta.Width; + originalHeight = meta.Height; + originalMipLevels = meta.MipLevels; + if (meta.MipLevels <= 1) + { + return Task.FromResult(false); + } + + if (!ShouldTrim(meta, targetMaxDimension)) + { + return Task.FromResult(false); + } + + var targetSize = CalculateTargetSize(meta.Width, meta.Height, targetMaxDimension); + dropCount = CalculateDropCount(meta, targetSize.width, targetSize.height); + if (dropCount <= 0) + { + return Task.FromResult(false); + } + + using var trimmed = TrimMipChain(original, dropCount); + TexFileHelper.Save(destination, trimmed); + RegisterDownscaledTexture(hash, sourcePath, destination); + _logger.LogDebug("Trimmed {DropCount} top mip level(s) for texture {Hash} -> {Path}", dropCount, hash, destination); + return Task.FromResult(true); + } + catch (Exception ex) + { + _logger.LogWarning( + ex, + "Failed to trim mips for texture {Hash} from {SourcePath} -> {Destination}. TargetMax={TargetMaxDimension}, OnlyUncompressed={OnlyDownscaleUncompressed}, HeaderFormat={HeaderFormat}, OriginalSize={OriginalWidth}x{OriginalHeight}, OriginalMipLevels={OriginalMipLevels}, DropAttempt={DropCount}", + hash, + sourcePath, + destination, + targetMaxDimension, + onlyDownscaleUncompressed, + header?.Format, + originalWidth, + originalHeight, + originalMipLevels, + dropCount); + TryDelete(destination); + return Task.FromResult(false); + } + } + + private static int CalculateDropCount(in TexMeta meta, int targetWidth, int targetHeight) + { + var drop = 0; + var width = meta.Width; + var height = meta.Height; + + while ((width > targetWidth || height > targetHeight) && drop + 1 < meta.MipLevels) + { + drop++; + width = ReduceDimension(width); + height = ReduceDimension(height); + } + + return drop; + } + + private static ScratchImage TrimMipChain(ScratchImage source, int dropCount) + { + var meta = source.Meta; + var newMeta = meta; + newMeta.MipLevels = meta.MipLevels - dropCount; + newMeta.Width = ReduceDimension(meta.Width, dropCount); + newMeta.Height = ReduceDimension(meta.Height, dropCount); + if (meta.Dimension == TexDimension.Tex3D) + { + newMeta.Depth = ReduceDimension(meta.Depth, dropCount); + } + + var result = ScratchImage.Initialize(newMeta); + CopyMipChainData(source, result, dropCount, meta); + return result; + } + + private static unsafe void CopyMipChainData(ScratchImage source, ScratchImage destination, int dropCount, in TexMeta sourceMeta) + { + var destinationMeta = destination.Meta; + var arraySize = Math.Max(1, sourceMeta.ArraySize); + var isCube = sourceMeta.IsCubeMap; + var isVolume = sourceMeta.Dimension == TexDimension.Tex3D; + + for (var item = 0; item < arraySize; item++) + { + for (var mip = 0; mip < destinationMeta.MipLevels; mip++) + { + var sourceMip = mip + dropCount; + var sliceCount = GetSliceCount(sourceMeta, sourceMip, isCube, isVolume); + + for (var slice = 0; slice < sliceCount; slice++) + { + var srcImage = source.GetImage(sourceMip, item, slice); + var dstImage = destination.GetImage(mip, item, slice); + CopyImage(srcImage, dstImage); + } + } + } + } + + private static int GetSliceCount(in TexMeta meta, int mip, bool isCube, bool isVolume) + { + if (isCube) + { + return 6; + } + + if (isVolume) + { + return Math.Max(1, meta.Depth >> mip); + } + + return 1; + } + + private static unsafe void CopyImage(in OtterImage source, in OtterImage destination) + { + var srcPtr = (byte*)source.Pixels; + var dstPtr = (byte*)destination.Pixels; + var bytesToCopy = Math.Min(source.SlicePitch, destination.SlicePitch); + Buffer.MemoryCopy(srcPtr, dstPtr, destination.SlicePitch, bytesToCopy); + } + + private static int ReduceDimension(int value, int iterations) + { + var result = value; + for (var i = 0; i < iterations; i++) + { + result = ReduceDimension(result); + } + + return result; + } + + private static int ReduceDimension(int value) + => value <= 1 ? 1 : Math.Max(1, value / 2); + + private static bool ShouldTrim(in TexMeta meta, int targetMaxDimension) + { + var depth = meta.Dimension == TexDimension.Tex3D ? Math.Max(1, meta.Depth) : 1; + return ShouldTrimDimensions(meta.Width, meta.Height, depth, targetMaxDimension); + } + + private static bool ShouldTrimDimensions(int width, int height, int depth, int targetMaxDimension) + { + if (width <= targetMaxDimension && height <= targetMaxDimension && depth <= targetMaxDimension) + { + return false; + } + + return true; + } + + private int ResolveTargetMaxDimension() + { + var configured = _playerPerformanceConfigService.Current.TextureDownscaleMaxDimension; + if (configured <= 0) + { + return DefaultTargetMaxDimension; + } + + return Math.Clamp(configured, BlockMultiple, MaxSupportedTargetDimension); + } + + private readonly struct TexHeaderInfo + { + public TexHeaderInfo(ushort width, ushort height, ushort depth, ushort mipLevels, TexFile.TextureFormat format) + { + Width = width; + Height = height; + Depth = depth; + MipLevels = mipLevels; + Format = format; + } + + public ushort Width { get; } + public ushort Height { get; } + public ushort Depth { get; } + public ushort MipLevels { get; } + public TexFile.TextureFormat Format { get; } + } + + private static bool TryReadTexHeader(string path, out TexHeaderInfo header) + { + header = default; + + try + { + Span buffer = stackalloc byte[16]; + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + var read = stream.Read(buffer); + if (read < buffer.Length) + { + return false; + } + + var formatValue = BinaryPrimitives.ReadInt32LittleEndian(buffer[4..8]); + var format = (TexFile.TextureFormat)formatValue; + var width = BinaryPrimitives.ReadUInt16LittleEndian(buffer[8..10]); + var height = BinaryPrimitives.ReadUInt16LittleEndian(buffer[10..12]); + var depth = BinaryPrimitives.ReadUInt16LittleEndian(buffer[12..14]); + var mipLevels = BinaryPrimitives.ReadUInt16LittleEndian(buffer[14..16]); + header = new TexHeaderInfo(width, height, depth, mipLevels, format); + return true; + } + catch + { + return false; + } + } + + private static bool IsBlockCompressedFormat(TexFile.TextureFormat format) + => TryGetCompressionTarget(format, out _); + + private static bool TryGetCompressionTarget(TexFile.TextureFormat format, out TextureCompressionTarget target) + { + if (BlockCompressedFormatMap.TryGetValue(unchecked((int)format), out var mapped)) + { + target = mapped; + return true; + } + + target = default; + return false; + } + + private void RegisterDownscaledTexture(string hash, string sourcePath, string destination) + { + _downscaledPaths[hash] = destination; + _logger.LogDebug("Downscaled texture {Hash} -> {Path}", hash, destination); + + var performanceConfig = _playerPerformanceConfigService.Current; + if (performanceConfig.KeepOriginalTextureFiles) + { + return; + } + + if (string.Equals(sourcePath, destination, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + if (!TryReplaceCacheEntryWithDownscaled(hash, sourcePath, destination)) + { + return; + } + + TryDelete(sourcePath); + } + + private bool TryReplaceCacheEntryWithDownscaled(string hash, string sourcePath, string destination) + { + try + { + var cacheEntry = _fileCacheManager.GetFileCacheByHash(hash); + if (cacheEntry is null || !cacheEntry.IsCacheEntry) + { + return File.Exists(sourcePath) ? false : true; + } + + var cacheFolder = _configService.Current.CacheFolder; + if (string.IsNullOrEmpty(cacheFolder)) + { + return false; + } + + if (!destination.StartsWith(cacheFolder, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var info = new FileInfo(destination); + if (!info.Exists) + { + return false; + } + + var relative = Path.GetRelativePath(cacheFolder, destination) + .Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + var sanitizedRelative = relative.TrimStart(Path.DirectorySeparatorChar); + var prefixed = Path.Combine(FileCacheManager.CachePrefix, sanitizedRelative); + + var replacement = new FileCacheEntity( + hash, + prefixed, + info.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), + info.Length, + cacheEntry.CompressedSize); + replacement.SetResolvedFilePath(destination); + + if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase)) + { + _fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath); + } + + _fileCacheManager.UpdateHashedFile(replacement, computeProperties: false); + _fileCacheManager.WriteOutFullCsv(); + + _logger.LogTrace("Replaced cache entry for texture {Hash} to downscaled path {Path}", hash, destination); + return true; + } + catch (Exception ex) + { + _logger.LogTrace(ex, "Failed to replace cache entry for texture {Hash}", hash); + return false; + } + } + + private string? GetExistingDownscaledPath(string hash) + { + var candidate = Path.Combine(GetDownscaledDirectory(), $"{hash}.tex"); + return File.Exists(candidate) ? candidate : null; + } + + private string GetDownscaledDirectory() + { + var directory = Path.Combine(_configService.Current.CacheFolder, "downscaled"); + if (!Directory.Exists(directory)) + { + try + { + Directory.CreateDirectory(directory); + } + catch (Exception ex) + { + _logger.LogTrace(ex, "Failed to create downscaled directory {Directory}", directory); + } + } + + return directory; + } + + private static void TryDelete(string? path) + { + if (string.IsNullOrEmpty(path)) return; + try + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + catch + { + // ignored + } + } + +} diff --git a/LightlessSync/Services/TextureCompression/TextureMapKind.cs b/LightlessSync/Services/TextureCompression/TextureMapKind.cs new file mode 100644 index 0000000..b007613 --- /dev/null +++ b/LightlessSync/Services/TextureCompression/TextureMapKind.cs @@ -0,0 +1,11 @@ +namespace LightlessSync.Services.TextureCompression; + +public enum TextureMapKind +{ + Diffuse, + Normal, + Specular, + Mask, + Index, + Unknown +} diff --git a/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs b/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs new file mode 100644 index 0000000..20d9a8f --- /dev/null +++ b/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs @@ -0,0 +1,590 @@ +using Dalamud.Plugin.Services; +using Microsoft.Extensions.Logging; +using Penumbra.GameData.Files; + +namespace LightlessSync.Services.TextureCompression; + +// ima lie, this isn't garbage + +public sealed class TextureMetadataHelper +{ + private readonly ILogger _logger; + private readonly IDataManager _dataManager; + + private static readonly Dictionary RecommendationCatalog = new() + { + [TextureCompressionTarget.BC1] = ( + "BC1 (Simple Compression for Opaque RGB)", + "This offers a 8:1 compression ratio and is quick with acceptable quality, but only supports RGB, without Alpha.\n\nCan be used for diffuse maps and equipment textures to save extra space."), + [TextureCompressionTarget.BC3] = ( + "BC3 (Simple Compression for RGBA)", + "This offers a 4:1 compression ratio and is quick with acceptable quality, and fully supports RGBA.\n\nGeneric format that can be used for most textures."), + [TextureCompressionTarget.BC4] = ( + "BC4 (Simple Compression for Opaque Grayscale)", + "This offers a 8:1 compression ratio and has almost indistinguishable quality, but only supports Grayscale, without Alpha.\n\nCan be used for face paints and legacy marks."), + [TextureCompressionTarget.BC5] = ( + "BC5 (Simple Compression for Opaque RG)", + "This offers a 4:1 compression ratio and has almost indistinguishable quality, but only supports RG, without B or Alpha.\n\nRecommended for index maps, unrecommended for normal maps."), + [TextureCompressionTarget.BC7] = ( + "BC7 (Complex Compression for RGBA)", + "This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while.\n\nGeneric format that can be used for most textures.") + }; + + private static readonly (TextureUsageCategory Category, string Token)[] CategoryTokens = + { + (TextureUsageCategory.UI, "/ui/"), + (TextureUsageCategory.UI, "/uld/"), + (TextureUsageCategory.UI, "/icon/"), + + (TextureUsageCategory.VisualEffect, "/vfx/"), + + (TextureUsageCategory.Customization, "/chara/human/"), + (TextureUsageCategory.Customization, "/chara/common/"), + (TextureUsageCategory.Customization, "/chara/bibo"), + + (TextureUsageCategory.Weapon, "/chara/weapon/"), + + (TextureUsageCategory.Accessory, "/chara/accessory/"), + + (TextureUsageCategory.Gear, "/chara/equipment/"), + + (TextureUsageCategory.Monster, "/chara/monster/"), + (TextureUsageCategory.Monster, "/chara/demihuman/"), + + (TextureUsageCategory.MountOrMinion, "/chara/mount/"), + (TextureUsageCategory.MountOrMinion, "/chara/battlepet/"), + + (TextureUsageCategory.Companion, "/chara/companion/"), + + (TextureUsageCategory.Housing, "/hou/"), + (TextureUsageCategory.Housing, "/housing/"), + (TextureUsageCategory.Housing, "/bg/"), + (TextureUsageCategory.Housing, "/bgcommon/") + }; + + private static readonly (TextureUsageCategory Category, string SlotToken, string SlotName)[] SlotTokens = + { + (TextureUsageCategory.Gear, "_met", "Head"), + + (TextureUsageCategory.Gear, "_top", "Body"), + + (TextureUsageCategory.Gear, "_glv", "Hands"), + + (TextureUsageCategory.Gear, "_dwn", "Legs"), + + (TextureUsageCategory.Gear, "_sho", "Feet"), + + (TextureUsageCategory.Accessory, "_ear", "Ears"), + + (TextureUsageCategory.Accessory, "_nek", "Neck"), + + (TextureUsageCategory.Accessory, "_wrs", "Wrists"), + + (TextureUsageCategory.Accessory, "_rir", "Ring"), + + (TextureUsageCategory.Weapon, "_w", "Weapon"), // sussy + (TextureUsageCategory.Weapon, "weapon", "Weapon"), + }; + + private static readonly (TextureMapKind Kind, string Token)[] MapTokens = + { + (TextureMapKind.Normal, "_n."), + (TextureMapKind.Normal, "_n_"), + (TextureMapKind.Normal, "_normal"), + (TextureMapKind.Normal, "normal_"), + (TextureMapKind.Normal, "_norm"), + (TextureMapKind.Normal, "norm_"), + + (TextureMapKind.Mask, "_m."), + (TextureMapKind.Mask, "_m_"), + (TextureMapKind.Mask, "_mask"), + (TextureMapKind.Mask, "mask_"), + (TextureMapKind.Mask, "_msk"), + + (TextureMapKind.Specular, "_s."), + (TextureMapKind.Specular, "_s_"), + (TextureMapKind.Specular, "_spec"), + (TextureMapKind.Specular, "_specular"), + (TextureMapKind.Specular, "specular_"), + + (TextureMapKind.Index, "_id."), + (TextureMapKind.Index, "_id_"), + (TextureMapKind.Index, "_idx"), + (TextureMapKind.Index, "_index"), + (TextureMapKind.Index, "index_"), + (TextureMapKind.Index, "_multi"), + + (TextureMapKind.Diffuse, "_d."), + (TextureMapKind.Diffuse, "_d_"), + (TextureMapKind.Diffuse, "_diff"), + (TextureMapKind.Diffuse, "_b."), + (TextureMapKind.Diffuse, "_b_"), + (TextureMapKind.Diffuse, "_base"), + (TextureMapKind.Diffuse, "base_") + }; + + private const string TextureSegment = "/texture/"; + private const string MaterialSegment = "/material/"; + + private const uint NormalSamplerId = ShpkFile.NormalSamplerId; + private const uint IndexSamplerId = ShpkFile.IndexSamplerId; + private const uint SpecularSamplerId = ShpkFile.SpecularSamplerId; + private const uint DiffuseSamplerId = ShpkFile.DiffuseSamplerId; + private const uint MaskSamplerId = ShpkFile.MaskSamplerId; + + public TextureMetadataHelper(ILogger logger, IDataManager dataManager) + { + _logger = logger; + _dataManager = dataManager; + } + + public static bool TryGetRecommendationInfo(TextureCompressionTarget target, out (string Title, string Description) info) + => RecommendationCatalog.TryGetValue(target, out info); + + public static TextureUsageCategory DetermineCategory(string? gamePath) + { + var normalized = Normalize(gamePath); + if (string.IsNullOrEmpty(normalized)) + return TextureUsageCategory.Unknown; + + var fileName = Path.GetFileName(normalized); + if (!string.IsNullOrEmpty(fileName)) + { + if (fileName.StartsWith("bibo", StringComparison.OrdinalIgnoreCase) + || fileName.Contains("gen3", StringComparison.OrdinalIgnoreCase) + || fileName.Contains("tfgen3", StringComparison.OrdinalIgnoreCase)) + { + return TextureUsageCategory.Customization; + } + } + + if (normalized.Contains("bibo", StringComparison.OrdinalIgnoreCase) + || normalized.Contains("skin", StringComparison.OrdinalIgnoreCase) + || normalized.Contains("gen3", StringComparison.OrdinalIgnoreCase) + || normalized.Contains("tfgen3", StringComparison.OrdinalIgnoreCase) + || normalized.Contains("body", StringComparison.OrdinalIgnoreCase)) + { + return TextureUsageCategory.Customization; + } + + foreach (var (category, token) in CategoryTokens) + { + if (normalized.Contains(token, StringComparison.OrdinalIgnoreCase)) + return category; + } + + var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries); + if (segments.Length >= 2 && string.Equals(segments[0], "chara", StringComparison.OrdinalIgnoreCase)) + { + return segments[1] switch + { + "equipment" => TextureUsageCategory.Gear, + "accessory" => TextureUsageCategory.Accessory, + "weapon" => TextureUsageCategory.Weapon, + "human" or "common" => TextureUsageCategory.Customization, + "monster" or "demihuman" => TextureUsageCategory.Monster, + "mount" or "battlepet" => TextureUsageCategory.MountOrMinion, + "companion" => TextureUsageCategory.Companion, + _ => TextureUsageCategory.Unknown + }; + } + + if (normalized.StartsWith("chara/", StringComparison.OrdinalIgnoreCase) + && (normalized.Contains("bibo", StringComparison.OrdinalIgnoreCase) + || normalized.Contains("skin", StringComparison.OrdinalIgnoreCase) + || normalized.Contains("body", StringComparison.OrdinalIgnoreCase))) + return TextureUsageCategory.Customization; + + return TextureUsageCategory.Unknown; + } + + public static string DetermineSlot(TextureUsageCategory category, string? gamePath) + { + if (category == TextureUsageCategory.Customization) + return GuessCustomizationSlot(gamePath); + + var normalized = Normalize(gamePath); + var fileName = Path.GetFileNameWithoutExtension(normalized); + var searchSource = $"{normalized} {fileName}".ToLowerInvariant(); + + foreach (var (candidateCategory, token, slot) in SlotTokens) + { + if (candidateCategory == category && searchSource.Contains(token, StringComparison.Ordinal)) + return slot; + } + + return category switch + { + TextureUsageCategory.Gear => "Gear", + TextureUsageCategory.Accessory => "Accessory", + TextureUsageCategory.Weapon => "Weapon", + TextureUsageCategory.Monster => "Monster", + TextureUsageCategory.MountOrMinion => "Mount / Minion", + TextureUsageCategory.Companion => "Companion", + TextureUsageCategory.VisualEffect => "VFX", + TextureUsageCategory.Housing => "Housing", + TextureUsageCategory.UI => "UI", + _ => "General" + }; + } + + public TextureMapKind DetermineMapKind(string path) + => DetermineMapKind(path, null); + + public TextureMapKind DetermineMapKind(string? gamePath, string? localTexturePath) + { + if (TryDetermineFromMaterials(gamePath, localTexturePath, out var kind)) + return kind; + + return GuessMapFromFileName(gamePath ?? localTexturePath ?? string.Empty); + } + + private bool TryDetermineFromMaterials(string? gamePath, string? localTexturePath, out TextureMapKind kind) + { + kind = TextureMapKind.Unknown; + + var candidates = new List(); + AddGameMaterialCandidates(gamePath, candidates); + AddLocalMaterialCandidates(localTexturePath, candidates); + + if (candidates.Count == 0) + return false; + + var normalizedGamePath = Normalize(gamePath); + var normalizedFileName = Path.GetFileName(normalizedGamePath); + + foreach (var candidate in candidates) + { + if (!TryLoadMaterial(candidate, out var material)) + continue; + + if (TryInferKindFromMaterial(material, normalizedGamePath, normalizedFileName, out kind)) + return true; + } + + return false; + } + + private static void AddGameMaterialCandidates(string? gamePath, IList candidates) + { + var normalized = Normalize(gamePath); + if (string.IsNullOrEmpty(normalized)) + return; + + var textureIndex = normalized.IndexOf(TextureSegment, StringComparison.Ordinal); + if (textureIndex < 0) + return; + + var prefix = normalized[..textureIndex]; + var suffix = normalized[(textureIndex + TextureSegment.Length)..]; + var baseName = Path.GetFileNameWithoutExtension(suffix); + if (string.IsNullOrEmpty(baseName)) + return; + + var directory = $"{prefix}{MaterialSegment}{Path.GetDirectoryName(suffix)?.Replace('\\', '/') ?? string.Empty}".TrimEnd('/'); + candidates.Add(MaterialCandidate.Game($"{directory}/mt_{baseName}.mtrl")); + + if (baseName.StartsWith('v') && baseName.IndexOf('_') is > 0 and var idx) + { + var trimmed = baseName[(idx + 1)..]; + candidates.Add(MaterialCandidate.Game($"{directory}/mt_{trimmed}.mtrl")); + } + } + + private static void AddLocalMaterialCandidates(string? localTexturePath, IList candidates) + { + if (string.IsNullOrEmpty(localTexturePath)) + return; + + var normalized = localTexturePath.Replace('\\', '/'); + var textureIndex = normalized.IndexOf(TextureSegment, StringComparison.OrdinalIgnoreCase); + if (textureIndex >= 0) + { + var prefix = normalized[..textureIndex]; + var suffix = normalized[(textureIndex + TextureSegment.Length)..]; + var folder = Path.GetDirectoryName(suffix)?.Replace('\\', '/') ?? string.Empty; + var baseName = Path.GetFileNameWithoutExtension(suffix); + if (!string.IsNullOrEmpty(baseName)) + { + var materialDir = $"{prefix}{MaterialSegment}{folder}".TrimEnd('/'); + candidates.Add(MaterialCandidate.Local(Path.Combine(materialDir.Replace('/', Path.DirectorySeparatorChar), $"mt_{baseName}.mtrl"))); + + if (baseName.StartsWith('v') && baseName.IndexOf('_') is > 0 and var idx) + { + var trimmed = baseName[(idx + 1)..]; + candidates.Add(MaterialCandidate.Local(Path.Combine(materialDir.Replace('/', Path.DirectorySeparatorChar), $"mt_{trimmed}.mtrl"))); + } + } + } + + var textureDirectory = Path.GetDirectoryName(localTexturePath); + if (!string.IsNullOrEmpty(textureDirectory) && Directory.Exists(textureDirectory)) + { + foreach (var candidate in Directory.EnumerateFiles(textureDirectory, "*.mtrl", SearchOption.TopDirectoryOnly)) + candidates.Add(MaterialCandidate.Local(candidate)); + } + } + + private bool TryLoadMaterial(MaterialCandidate candidate, out MtrlFile material) + { + material = null!; + + try + { + switch (candidate.Source) + { + case MaterialSource.Game: + var gameFile = _dataManager.GetFile(candidate.Path); + if (gameFile?.Data.Length > 0) + { + material = new MtrlFile(gameFile.Data); + return material.Valid; + } + break; + + case MaterialSource.Local when File.Exists(candidate.Path): + material = new MtrlFile(File.ReadAllBytes(candidate.Path)); + return material.Valid; + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to load material {Path}", candidate.Path); + } + + return false; + } + + private static bool TryInferKindFromMaterial(MtrlFile material, string normalizedGamePath, string? fileName, out TextureMapKind kind) + { + kind = TextureMapKind.Unknown; + var targetName = fileName ?? string.Empty; + + foreach (var sampler in material.ShaderPackage.Samplers) + { + if (!TryMapSamplerId(sampler.SamplerId, out var candidateKind)) + continue; + + if (sampler.TextureIndex < 0 || sampler.TextureIndex >= material.Textures.Length) + continue; + + var texturePath = Normalize(material.Textures[sampler.TextureIndex].Path); + + if (!string.IsNullOrEmpty(normalizedGamePath) && string.Equals(texturePath, normalizedGamePath, StringComparison.OrdinalIgnoreCase)) + { + kind = candidateKind; + return true; + } + + if (!string.IsNullOrEmpty(targetName) && string.Equals(Path.GetFileName(texturePath), targetName, StringComparison.OrdinalIgnoreCase)) + { + kind = candidateKind; + return true; + } + } + + return false; + } + + private static TextureMapKind GuessMapFromFileName(string path) + { + var normalized = Normalize(path); + var fileNameWithExtension = Path.GetFileName(normalized); + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(normalized); + if (string.IsNullOrEmpty(fileNameWithExtension) && string.IsNullOrEmpty(fileNameWithoutExtension)) + return TextureMapKind.Unknown; + + foreach (var (kind, token) in MapTokens) + { + if (!string.IsNullOrEmpty(fileNameWithExtension) && + fileNameWithExtension.Contains(token, StringComparison.OrdinalIgnoreCase)) + return kind; + + if (!string.IsNullOrEmpty(fileNameWithoutExtension) && + fileNameWithoutExtension.Contains(token, StringComparison.OrdinalIgnoreCase)) + return kind; + } + + return TextureMapKind.Unknown; + } + + private static readonly (string Token, TextureCompressionTarget Target)[] FormatTargetTokens = + { + ("BC1", TextureCompressionTarget.BC1), + ("DXT1", TextureCompressionTarget.BC1), + ("BC3", TextureCompressionTarget.BC3), + ("DXT3", TextureCompressionTarget.BC3), + ("DXT5", TextureCompressionTarget.BC3), + ("BC4", TextureCompressionTarget.BC4), + ("ATI1", TextureCompressionTarget.BC4), + ("BC5", TextureCompressionTarget.BC5), + ("ATI2", TextureCompressionTarget.BC5), + ("3DC", TextureCompressionTarget.BC5), + ("BC7", TextureCompressionTarget.BC7), + ("BPTC", TextureCompressionTarget.BC7) + }; // idk man + + public static bool TryMapFormatToTarget(string? format, out TextureCompressionTarget target) + { + var normalized = (format ?? string.Empty).ToUpperInvariant(); + foreach (var (token, mappedTarget) in FormatTargetTokens) + { + if (normalized.Contains(token, StringComparison.Ordinal)) + { + target = mappedTarget; + return true; + } + } + + target = TextureCompressionTarget.BC7; + return false; + } + + public static (TextureCompressionTarget Target, string Reason)? GetSuggestedTarget( + string? format, + TextureMapKind mapKind, + string? texturePath = null) + { + TextureCompressionTarget? current = null; + if (TryMapFormatToTarget(format, out var mapped)) + current = mapped; + + var prefersBc4 = IsFacePaintOrMarkTexture(texturePath); + + var suggestion = mapKind switch + { + TextureMapKind.Normal => TextureCompressionTarget.BC7, + TextureMapKind.Mask => TextureCompressionTarget.BC7, + TextureMapKind.Index => TextureCompressionTarget.BC5, + TextureMapKind.Specular => TextureCompressionTarget.BC3, + TextureMapKind.Diffuse => TextureCompressionTarget.BC7, + _ => TextureCompressionTarget.BC7 + }; + + if (prefersBc4) + { + suggestion = TextureCompressionTarget.BC4; + } + else if (mapKind == TextureMapKind.Diffuse && current is null && !HasAlphaHint(format)) + suggestion = TextureCompressionTarget.BC1; + + if (current == suggestion) + return null; + + return (suggestion, RecommendationCatalog.TryGetValue(suggestion, out var info) + ? info.Description + : "Suggested to balance visual quality and file size."); + } + + private static bool TryMapSamplerId(uint id, out TextureMapKind kind) + { + kind = id switch + { + NormalSamplerId => TextureMapKind.Normal, + IndexSamplerId => TextureMapKind.Index, + SpecularSamplerId => TextureMapKind.Specular, + DiffuseSamplerId => TextureMapKind.Diffuse, + MaskSamplerId => TextureMapKind.Mask, + _ => TextureMapKind.Unknown + }; + + return kind != TextureMapKind.Unknown; + } + + private static string GuessCustomizationSlot(string? gamePath) + { + var normalized = Normalize(gamePath); + var fileName = Path.GetFileName(normalized); + + if (!string.IsNullOrEmpty(fileName)) + { + if (fileName.StartsWith("bibo", StringComparison.OrdinalIgnoreCase) + || fileName.Contains("gen3", StringComparison.OrdinalIgnoreCase) + || fileName.Contains("tfgen3", StringComparison.OrdinalIgnoreCase) + || fileName.Contains("skin", StringComparison.OrdinalIgnoreCase)) + { + return "Skin"; + } + } + + if (normalized.Contains("hair", StringComparison.OrdinalIgnoreCase)) + return "Hair"; + if (normalized.Contains("face", StringComparison.OrdinalIgnoreCase)) + return "Face"; + if (normalized.Contains("tail", StringComparison.OrdinalIgnoreCase)) + return "Tail"; + if (normalized.Contains("zear", StringComparison.OrdinalIgnoreCase)) + return "Ear"; + if (normalized.Contains("eye", StringComparison.OrdinalIgnoreCase)) + return "Eye"; + if (normalized.Contains("body", StringComparison.OrdinalIgnoreCase) + || normalized.Contains("skin", StringComparison.OrdinalIgnoreCase) + || normalized.Contains("bibo", StringComparison.OrdinalIgnoreCase)) + return "Skin"; + if (IsFacePaintPath(normalized)) + return "Face Paint"; + if (IsLegacyMarkPath(normalized)) + return "Legacy Mark"; + if (normalized.Contains("decal_equip", StringComparison.OrdinalIgnoreCase)) + return "Equipment Decal"; + + return "Customization"; + } + + private static bool IsFacePaintOrMarkTexture(string? texturePath) + { + var normalized = Normalize(texturePath); + return IsFacePaintPath(normalized) || IsLegacyMarkPath(normalized); + } + + private static bool IsFacePaintPath(string? normalizedPath) + { + if (string.IsNullOrEmpty(normalizedPath)) + return false; + + return normalizedPath.Contains("decal_face", StringComparison.Ordinal) + || normalizedPath.Contains("facepaint", StringComparison.Ordinal) + || normalizedPath.Contains("_decal_", StringComparison.Ordinal); + } + + private static bool IsLegacyMarkPath(string? normalizedPath) + { + if (string.IsNullOrEmpty(normalizedPath)) + return false; + + return normalizedPath.Contains("transparent", StringComparison.Ordinal) + || normalizedPath.Contains("transparent.tex", StringComparison.Ordinal); + } + + private static bool HasAlphaHint(string? format) + { + if (string.IsNullOrEmpty(format)) + return false; + + var normalized = format.ToUpperInvariant(); + return normalized.Contains("A8", StringComparison.Ordinal) + || normalized.Contains("ARGB", StringComparison.Ordinal) + || normalized.Contains("BC3", StringComparison.Ordinal) + || normalized.Contains("BC7", StringComparison.Ordinal); + } + + private static string Normalize(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + return string.Empty; + + return path.Replace('\\', '/').ToLowerInvariant(); + } + + private readonly record struct MaterialCandidate(string Path, MaterialSource Source) + { + public static MaterialCandidate Game(string path) => new(path, MaterialSource.Game); + public static MaterialCandidate Local(string path) => new(path, MaterialSource.Local); + } + + private enum MaterialSource + { + Game, + Local + } +} diff --git a/LightlessSync/Services/TextureCompression/TextureUsageCategory.cs b/LightlessSync/Services/TextureCompression/TextureUsageCategory.cs new file mode 100644 index 0000000..ac01393 --- /dev/null +++ b/LightlessSync/Services/TextureCompression/TextureUsageCategory.cs @@ -0,0 +1,16 @@ +namespace LightlessSync.Services.TextureCompression; + +public enum TextureUsageCategory +{ + Gear, + Weapon, + Accessory, + Customization, + MountOrMinion, + Companion, + Monster, + Housing, + UI, + VisualEffect, + Unknown +} diff --git a/LightlessSync/Services/UiFactory.cs b/LightlessSync/Services/UiFactory.cs index 248eae0..9b90830 100644 --- a/LightlessSync/Services/UiFactory.cs +++ b/LightlessSync/Services/UiFactory.cs @@ -1,10 +1,12 @@ -using Dalamud.Interface.ImGuiFileDialog; +using LightlessSync.API.Data; using LightlessSync.API.Dto.Group; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI; +using LightlessSync.UI.Tags; using LightlessSync.WebAPI; +using LightlessSync.UI.Services; using Microsoft.Extensions.Logging; namespace LightlessSync.Services; @@ -15,42 +17,94 @@ public class UiFactory private readonly LightlessMediator _lightlessMediator; private readonly ApiController _apiController; private readonly UiSharedService _uiSharedService; - private readonly PairManager _pairManager; + private readonly PairUiService _pairUiService; private readonly ServerConfigurationManager _serverConfigManager; private readonly LightlessProfileManager _lightlessProfileManager; private readonly PerformanceCollectorService _performanceCollectorService; - private readonly FileDialogManager _fileDialogManager; + private readonly ProfileTagService _profileTagService; + private readonly DalamudUtilService _dalamudUtilService; - public UiFactory(ILoggerFactory loggerFactory, LightlessMediator lightlessMediator, ApiController apiController, - UiSharedService uiSharedService, PairManager pairManager, ServerConfigurationManager serverConfigManager, - LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService, FileDialogManager fileDialogManager) + public UiFactory( + ILoggerFactory loggerFactory, + LightlessMediator lightlessMediator, + ApiController apiController, + UiSharedService uiSharedService, + PairUiService pairUiService, + ServerConfigurationManager serverConfigManager, + LightlessProfileManager lightlessProfileManager, + PerformanceCollectorService performanceCollectorService, + ProfileTagService profileTagService, + DalamudUtilService dalamudUtilService) { _loggerFactory = loggerFactory; _lightlessMediator = lightlessMediator; _apiController = apiController; _uiSharedService = uiSharedService; - _pairManager = pairManager; + _pairUiService = pairUiService; _serverConfigManager = serverConfigManager; _lightlessProfileManager = lightlessProfileManager; _performanceCollectorService = performanceCollectorService; - _fileDialogManager = fileDialogManager; + _profileTagService = profileTagService; + _dalamudUtilService = dalamudUtilService; } public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto) { - return new SyncshellAdminUI(_loggerFactory.CreateLogger(), _lightlessMediator, - _apiController, _uiSharedService, _pairManager, dto, _performanceCollectorService, _lightlessProfileManager, _fileDialogManager); + return new SyncshellAdminUI( + _loggerFactory.CreateLogger(), + _lightlessMediator, + _apiController, + _uiSharedService, + _pairUiService, + dto, + _performanceCollectorService, + _lightlessProfileManager); } public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair) - { - return new StandaloneProfileUi(_loggerFactory.CreateLogger(), _lightlessMediator, - _uiSharedService, _serverConfigManager, _lightlessProfileManager, _pairManager, pair, _performanceCollectorService); - } + => CreateStandaloneProfileUiInternal(pair, pair.UserData, null, false, null); + + public StandaloneProfileUi CreateStandaloneProfileUi(UserData userData) + => CreateStandaloneProfileUiInternal(null, userData, null, false, null); + + public StandaloneProfileUi CreateLightfinderProfileUi(UserData userData, string hashedCid) + => CreateStandaloneProfileUiInternal(null, userData, null, true, hashedCid); + + public StandaloneProfileUi CreateStandaloneGroupProfileUi(GroupData groupInfo) + => CreateStandaloneProfileUiInternal(null, null, groupInfo, false, null); public PermissionWindowUI CreatePermissionPopupUi(Pair pair) { - return new PermissionWindowUI(_loggerFactory.CreateLogger(), pair, - _lightlessMediator, _uiSharedService, _apiController, _performanceCollectorService); + return new PermissionWindowUI( + _loggerFactory.CreateLogger(), + pair, + _lightlessMediator, + _uiSharedService, + _apiController, + _performanceCollectorService); + } + + private StandaloneProfileUi CreateStandaloneProfileUiInternal( + Pair? pair, + UserData? userData, + GroupData? groupData, + bool isLightfinderContext, + string? lightfinderCid) + { + return new StandaloneProfileUi( + _loggerFactory.CreateLogger(), + _lightlessMediator, + _uiSharedService, + _serverConfigManager, + _profileTagService, + dalamudUtilService: _dalamudUtilService, + lightlessProfileManager: _lightlessProfileManager, + pairUiService: _pairUiService, + pair: pair, + userData: userData, + groupData: groupData, + isLightfinderContext: isLightfinderContext, + lightfinderCid: lightfinderCid, + performanceCollector: _performanceCollectorService); } } diff --git a/LightlessSync/Services/UiService.cs b/LightlessSync/Services/UiService.cs index f08b1fc..16f0f4f 100644 --- a/LightlessSync/Services/UiService.cs +++ b/LightlessSync/Services/UiService.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Windowing; using LightlessSync.LightlessConfiguration; +using LightlessSync.PlayerData.Factories; using LightlessSync.Services.Mediator; using LightlessSync.UI; using LightlessSync.UI.Style; @@ -18,12 +19,13 @@ public sealed class UiService : DisposableMediatorSubscriberBase private readonly LightlessConfigService _lightlessConfigService; private readonly WindowSystem _windowSystem; private readonly UiFactory _uiFactory; + private readonly PairFactory _pairFactory; public UiService(ILogger logger, IUiBuilder uiBuilder, LightlessConfigService lightlessConfigService, WindowSystem windowSystem, IEnumerable windows, UiFactory uiFactory, FileDialogManager fileDialogManager, - LightlessMediator lightlessMediator) : base(logger, lightlessMediator) + LightlessMediator lightlessMediator, PairFactory pairFactory) : base(logger, lightlessMediator) { _logger = logger; _logger.LogTrace("Creating {type}", GetType().Name); @@ -31,6 +33,7 @@ public sealed class UiService : DisposableMediatorSubscriberBase _lightlessConfigService = lightlessConfigService; _windowSystem = windowSystem; _uiFactory = uiFactory; + _pairFactory = pairFactory; _fileDialogManager = fileDialogManager; _uiBuilder.DisableGposeUiHide = true; @@ -45,10 +48,101 @@ public sealed class UiService : DisposableMediatorSubscriberBase Mediator.Subscribe(this, (msg) => { + var resolvedPair = _pairFactory.Create(msg.Pair.UniqueIdent) ?? msg.Pair; if (!_createdWindows.Exists(p => p is StandaloneProfileUi ui - && string.Equals(ui.Pair.UserData.AliasOrUID, msg.Pair.UserData.AliasOrUID, StringComparison.Ordinal))) + && ui.Pair != null + && ui.Pair.UniqueIdent == resolvedPair.UniqueIdent)) { - var window = _uiFactory.CreateStandaloneProfileUi(msg.Pair); + var window = _uiFactory.CreateStandaloneProfileUi(resolvedPair); + _createdWindows.Add(window); + _windowSystem.AddWindow(window); + } + }); + + Mediator.Subscribe(this, msg => + { + var existingWindow = _createdWindows.Find(p => p is StandaloneProfileUi ui + && ui.IsGroupProfile + && ui.ProfileGroupData is not null + && string.Equals(ui.ProfileGroupData.GID, msg.Group.GID, StringComparison.Ordinal)); + + if (existingWindow is StandaloneProfileUi existing) + { + existing.IsOpen = true; + } + else + { + var window = _uiFactory.CreateStandaloneGroupProfileUi(msg.Group); + _createdWindows.Add(window); + _windowSystem.AddWindow(window); + } + }); + + Mediator.Subscribe(this, msg => + { + var window = _createdWindows.Find(p => p is StandaloneProfileUi ui + && ui.IsGroupProfile + && ui.ProfileGroupData is not null + && string.Equals(ui.ProfileGroupData.GID, msg.Group.Group.GID, StringComparison.Ordinal)); + + if (window is not null) + { + _windowSystem.RemoveWindow(window); + _createdWindows.Remove(window); + window.Dispose(); + } + }); + + Mediator.Subscribe(this, msg => + { + if (!_createdWindows.Exists(p => p is StandaloneProfileUi ui + && ui.Pair is null + && !ui.IsGroupProfile + && !ui.IsLightfinderContext + && string.Equals(ui.ProfileUserData.UID, msg.User.UID, StringComparison.Ordinal))) + { + var window = _uiFactory.CreateStandaloneProfileUi(msg.User); + _createdWindows.Add(window); + _windowSystem.AddWindow(window); + } + }); + + Mediator.Subscribe(this, msg => + { + var window = _createdWindows.Find(p => p is StandaloneProfileUi ui + && ui.Pair is null + && !ui.IsGroupProfile + && !ui.IsLightfinderContext + && string.Equals(ui.ProfileUserData.UID, msg.User.UID, StringComparison.Ordinal)); + + if (window is not null) + { + _windowSystem.RemoveWindow(window); + _createdWindows.Remove(window); + window.Dispose(); + } + }); + + Mediator.Subscribe(this, msg => + { + if (!_createdWindows.Exists(p => p is StandaloneProfileUi ui && ui.IsLightfinderContext && string.Equals(ui.LightfinderCid, msg.HashedCid, StringComparison.Ordinal))) + { + var window = _uiFactory.CreateLightfinderProfileUi(msg.User, msg.HashedCid); + _createdWindows.Add(window); + _windowSystem.AddWindow(window); + } + }); + + Mediator.Subscribe(this, msg => + { + if (!_createdWindows.Exists(p => p is StandaloneProfileUi ui + && !ui.IsLightfinderContext + && !ui.IsGroupProfile + && ui.Pair is null + && ui.ProfileUserData is not null + && string.Equals(ui.ProfileUserData.UID, msg.User.UID, StringComparison.Ordinal))) + { + var window = _uiFactory.CreateStandaloneProfileUi(msg.User); _createdWindows.Add(window); _windowSystem.AddWindow(window); } @@ -67,10 +161,12 @@ public sealed class UiService : DisposableMediatorSubscriberBase Mediator.Subscribe(this, (msg) => { + var resolvedPair = _pairFactory.Create(msg.Pair.UniqueIdent) ?? msg.Pair; if (!_createdWindows.Exists(p => p is PermissionWindowUI ui - && msg.Pair == ui.Pair)) + && ui.Pair is not null + && ui.Pair.UniqueIdent == resolvedPair.UniqueIdent)) { - var window = _uiFactory.CreatePermissionPopupUi(msg.Pair); + var window = _uiFactory.CreatePermissionPopupUi(resolvedPair); _createdWindows.Add(window); _windowSystem.AddWindow(window); } diff --git a/LightlessSync/Services/XivDataAnalyzer.cs b/LightlessSync/Services/XivDataAnalyzer.cs index db721a2..9d32883 100644 --- a/LightlessSync/Services/XivDataAnalyzer.cs +++ b/LightlessSync/Services/XivDataAnalyzer.cs @@ -46,7 +46,7 @@ public sealed class XivDataAnalyzer if (handle->FileName.Length > 1024) continue; var skeletonName = handle->FileName.ToString(); if (string.IsNullOrEmpty(skeletonName)) continue; - outputIndices[skeletonName] = new(); + outputIndices[skeletonName] = []; for (ushort boneIdx = 0; boneIdx < curBones; boneIdx++) { var boneName = handle->HavokSkeleton->Bones[boneIdx].Name.String; @@ -70,7 +70,7 @@ public sealed class XivDataAnalyzer var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash); if (cacheEntity == null) return null; - using BinaryReader reader = new BinaryReader(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read)); + using BinaryReader reader = new(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read)); // most of this shit is from vfxeditor, surely nothing will change in the pap format :copium: reader.ReadInt32(); // ignore @@ -177,17 +177,18 @@ public sealed class XivDataAnalyzer } long tris = 0; - for (int i = 0; i < file.LodCount; i++) + foreach (var lod in file.Lods) { try { - var meshIdx = file.Lods[i].MeshIndex; - var meshCnt = file.Lods[i].MeshCount; + var meshIdx = lod.MeshIndex; + var meshCnt = lod.MeshCount; + tris = file.Meshes.Skip(meshIdx).Take(meshCnt).Sum(p => p.IndexCount) / 3; } catch (Exception ex) { - _logger.LogDebug(ex, "Could not load lod mesh {mesh} from path {path}", i, filePath); + _logger.LogDebug(ex, "Could not load lod mesh {mesh} from path {path}", lod.MeshIndex, filePath); continue; } diff --git a/LightlessSync/UI/CharaDataHubUi.Functions.cs b/LightlessSync/UI/CharaDataHubUi.Functions.cs index 665e640..ccef174 100644 --- a/LightlessSync/UI/CharaDataHubUi.Functions.cs +++ b/LightlessSync/UI/CharaDataHubUi.Functions.cs @@ -13,13 +13,15 @@ internal sealed partial class CharaDataHubUi AccessTypeDto.AllPairs => "All Pairs", AccessTypeDto.ClosePairs => "Direct Pairs", AccessTypeDto.Individuals => "Specified", - AccessTypeDto.Public => "Everyone" + AccessTypeDto.Public => "Everyone", + _ => throw new NotSupportedException() }; private static string GetShareTypeString(ShareTypeDto dto) => dto switch { ShareTypeDto.Private => "Code Only", - ShareTypeDto.Shared => "Shared" + ShareTypeDto.Shared => "Shared", + _ => throw new NotSupportedException() }; private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry) @@ -31,7 +33,7 @@ internal sealed partial class CharaDataHubUi private void GposeMetaInfoAction(Action gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning) { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new(); sb.AppendLine(actionDescription); bool isDisabled = false; diff --git a/LightlessSync/UI/CharaDataHubUi.McdOnline.cs b/LightlessSync/UI/CharaDataHubUi.McdOnline.cs index e86ef10..0219205 100644 --- a/LightlessSync/UI/CharaDataHubUi.McdOnline.cs +++ b/LightlessSync/UI/CharaDataHubUi.McdOnline.cs @@ -406,7 +406,7 @@ internal sealed partial class CharaDataHubUi { _uiSharedService.BigText("Poses"); var poseCount = updateDto.PoseList.Count(); - using (ImRaii.Disabled(poseCount >= maxPoses)) + using (ImRaii.Disabled(poseCount >= _maxPoses)) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add new Pose")) { @@ -414,8 +414,8 @@ internal sealed partial class CharaDataHubUi } } ImGui.SameLine(); - using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"), poseCount == maxPoses)) - ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached"); + using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"), poseCount == _maxPoses)) + ImGui.TextUnformatted($"{poseCount}/{_maxPoses} poses attached"); ImGuiHelpers.ScaledDummy(5); using var indent = ImRaii.PushIndent(10f); @@ -463,12 +463,16 @@ internal sealed partial class CharaDataHubUi else { var desc = pose.Description; - if (ImGui.InputTextWithHint("##description", "Description", ref desc, 100)) + if (desc != null) { - pose.Description = desc; - updateDto.UpdatePoseList(); + if (ImGui.InputTextWithHint("##description", "Description", ref desc, 100)) + { + pose.Description = desc; + updateDto.UpdatePoseList(); + } + ImGui.SameLine(); } - ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete")) { updateDto.RemovePose(pose); @@ -795,11 +799,12 @@ internal sealed partial class CharaDataHubUi { UiSharedService.DrawTree("Access for Specific Individuals / Syncshells", () => { + var snapshot = _pairUiService.GetSnapshot(); using (ImRaii.PushId("user")) { using (ImRaii.Group()) { - InputComboHybrid("##AliasToAdd", "##AliasToAddPicker", ref _specificIndividualAdd, _pairManager.PairsWithGroups.Keys, + InputComboHybrid("##AliasToAdd", "##AliasToAddPicker", ref _specificIndividualAdd, snapshot.PairsWithGroups.Keys, static pair => (pair.UserData.UID, pair.UserData.Alias, pair.UserData.AliasOrUID, pair.GetNote())); ImGui.SameLine(); using (ImRaii.Disabled(string.IsNullOrEmpty(_specificIndividualAdd) @@ -868,8 +873,8 @@ internal sealed partial class CharaDataHubUi { using (ImRaii.Group()) { - InputComboHybrid("##GroupAliasToAdd", "##GroupAliasToAddPicker", ref _specificGroupAdd, _pairManager.Groups.Keys, - group => (group.GID, group.Alias, group.AliasOrGID, _serverConfigurationManager.GetNoteForGid(group.GID))); + InputComboHybrid("##GroupAliasToAdd", "##GroupAliasToAddPicker", ref _specificGroupAdd, snapshot.Groups, + group => (group.GID, group.GroupAliasOrGID, group.GroupAliasOrGID, _serverConfigurationManager.GetNoteForGid(group.GID))); ImGui.SameLine(); using (ImRaii.Disabled(string.IsNullOrEmpty(_specificGroupAdd) || updateDto.GroupList.Any(f => string.Equals(f.GID, _specificGroupAdd, StringComparison.Ordinal) || string.Equals(f.Alias, _specificGroupAdd, StringComparison.Ordinal)))) diff --git a/LightlessSync/UI/CharaDataHubUi.cs b/LightlessSync/UI/CharaDataHubUi.cs index 51723b9..fdaa27c 100644 --- a/LightlessSync/UI/CharaDataHubUi.cs +++ b/LightlessSync/UI/CharaDataHubUi.cs @@ -7,7 +7,6 @@ using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Dto.CharaData; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; -using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.CharaData; using LightlessSync.Services.CharaData.Models; @@ -15,24 +14,26 @@ using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.Utils; using Microsoft.Extensions.Logging; +using LightlessSync.UI.Services; +using LightlessSync.PlayerData.Pairs; namespace LightlessSync.UI; internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase { - private const int maxPoses = 10; + private const int _maxPoses = 10; private readonly CharaDataManager _charaDataManager; private readonly CharaDataNearbyManager _charaDataNearbyManager; private readonly CharaDataConfigService _configService; private readonly DalamudUtilService _dalamudUtilService; private readonly FileDialogManager _fileDialogManager; - private readonly PairManager _pairManager; + private readonly PairUiService _pairUiService; private readonly CharaDataGposeTogetherManager _charaDataGposeTogetherManager; private readonly ServerConfigurationManager _serverConfigurationManager; private readonly UiSharedService _uiSharedService; private CancellationTokenSource _closalCts = new(); private bool _disableUI = false; - private CancellationTokenSource _disposalCts = new(); + private readonly CancellationTokenSource _disposalCts = new(); private string _exportDescription = string.Empty; private string _filterCodeNote = string.Empty; private string _filterDescription = string.Empty; @@ -77,7 +78,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase public CharaDataHubUi(ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollectorService, CharaDataManager charaDataManager, CharaDataNearbyManager charaDataNearbyManager, CharaDataConfigService configService, UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager, - DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager, + DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairUiService pairUiService, CharaDataGposeTogetherManager charaDataGposeTogetherManager) : base(logger, mediator, "Lightless Sync Character Data Hub###LightlessSyncCharaDataUI", performanceCollectorService) { @@ -90,7 +91,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase _serverConfigurationManager = serverConfigurationManager; _dalamudUtilService = dalamudUtilService; _fileDialogManager = fileDialogManager; - _pairManager = pairManager; + _pairUiService = pairUiService; _charaDataGposeTogetherManager = charaDataGposeTogetherManager; Mediator.Subscribe(this, (_) => IsOpen |= _configService.Current.OpenLightlessHubOnGposeStart); Mediator.Subscribe(this, (msg) => @@ -144,6 +145,8 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase { _closalCts.CancelDispose(); _disposalCts.CancelDispose(); + _disposalCts.Dispose(); + _closalCts.Dispose(); } base.Dispose(disposing); diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index cc8d326..b1195b4 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -2,8 +2,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using Dalamud.Utility; -using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.Interop.Ipc; @@ -11,24 +9,26 @@ using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; +using LightlessSync.Services.LightFinder; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Components; using LightlessSync.UI.Handlers; using LightlessSync.UI.Models; +using LightlessSync.UI.Services; +using LightlessSync.UI.Style; using LightlessSync.Utils; using LightlessSync.WebAPI; using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.SignalR.Utils; using Microsoft.Extensions.Logging; -using System; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Globalization; -using System.Linq; using System.Numerics; using System.Reflection; +using System.Runtime.InteropServices; namespace LightlessSync.UI; @@ -38,11 +38,12 @@ public class CompactUi : WindowMediatorSubscriberBase private readonly ApiController _apiController; private readonly LightlessConfigService _configService; private readonly LightlessMediator _lightlessMediator; + private readonly PairLedger _pairLedger; private readonly ConcurrentDictionary> _currentDownloads = new(); private readonly DrawEntityFactory _drawEntityFactory; private readonly FileUploadManager _fileTransferManager; private readonly PlayerPerformanceConfigService _playerPerformanceConfig; - private readonly PairManager _pairManager; + private readonly PairUiService _pairUiService; private readonly SelectTagForPairUi _selectTagForPairUi; private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi; private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi; @@ -54,7 +55,8 @@ public class CompactUi : WindowMediatorSubscriberBase private readonly TopTabMenu _tabMenu; private readonly TagHandler _tagHandler; private readonly UiSharedService _uiSharedService; - private readonly BroadcastService _broadcastService; + private readonly LightFinderService _broadcastService; + private readonly DalamudUtilService _dalamudUtilService; private List _drawFolders; private Pair? _lastAddedUser; @@ -65,13 +67,18 @@ public class CompactUi : WindowMediatorSubscriberBase private float _transferPartHeight; private bool _wasOpen; private float _windowContentWidth; + private readonly SeluneBrush _seluneBrush = new(); + private const float _connectButtonHighlightThickness = 14f; + private Pair? _focusedPair; + private Pair? _pendingFocusPair; + private int _pendingFocusFrame = -1; public CompactUi( ILogger logger, UiSharedService uiShared, LightlessConfigService configService, ApiController apiController, - PairManager pairManager, + PairUiService pairUiService, ServerConfigurationManager serverManager, LightlessMediator mediator, FileUploadManager fileTransferManager, @@ -85,14 +92,14 @@ public class CompactUi : WindowMediatorSubscriberBase RenameSyncshellTagUi renameSyncshellTagUi, PerformanceCollectorService performanceCollectorService, IpcManager ipcManager, - BroadcastService broadcastService, + LightFinderService broadcastService, CharacterAnalyzer characterAnalyzer, - PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) + PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService, PairLedger pairLedger, LightFinderScannerService lightFinderScannerService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) { _uiSharedService = uiShared; _configService = configService; _apiController = apiController; - _pairManager = pairManager; + _pairUiService = pairUiService; _serverManager = serverManager; _fileTransferManager = fileTransferManager; _tagHandler = tagHandler; @@ -105,43 +112,19 @@ public class CompactUi : WindowMediatorSubscriberBase _renamePairTagUi = renameTagUi; _ipcManager = ipcManager; _broadcastService = broadcastService; - _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService); + _pairLedger = pairLedger; + _dalamudUtilService = dalamudUtilService; + _tabMenu = new TopTabMenu(Mediator, _apiController, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService, broadcastService, lightFinderScannerService); + Mediator.Subscribe(this, msg => RegisterFocusCharacter(msg.Pair)); - AllowPinning = true; - AllowClickthrough = false; - TitleBarButtons = new() - { - new TitleBarButton() - { - Icon = FontAwesomeIcon.Cog, - Click = (msg) => - { - Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))); - }, - IconOffset = new(2,1), - ShowTooltip = () => - { - ImGui.BeginTooltip(); - ImGui.Text("Open Lightless Settings"); - ImGui.EndTooltip(); - } - }, - new TitleBarButton() - { - Icon = FontAwesomeIcon.Book, - Click = (msg) => - { - Mediator.Publish(new UiToggleMessage(typeof(EventViewerUI))); - }, - IconOffset = new(2,1), - ShowTooltip = () => - { - ImGui.BeginTooltip(); - ImGui.Text("Open Lightless Event Viewer"); - ImGui.EndTooltip(); - } - }, - }; + WindowBuilder.For(this) + .AllowPinning(true) + .AllowClickthrough(false) + .SetSizeConstraints(new Vector2(375, 400), new Vector2(375, 2000)) + .AddFlags(ImGuiWindowFlags.NoDocking) + .AddTitleBarButton(FontAwesomeIcon.Cog, "Open Lightless Settings", () => Mediator.Publish(new UiToggleMessage(typeof(SettingsUi)))) + .AddTitleBarButton(FontAwesomeIcon.Book, "Open Lightless Event Viewer", () => Mediator.Publish(new UiToggleMessage(typeof(EventViewerUI)))) + .Apply(); _drawFolders = [.. DrawFolders]; @@ -162,20 +145,24 @@ public class CompactUi : WindowMediatorSubscriberBase Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); Mediator.Subscribe(this, (msg) => _drawFolders = DrawFolders.ToList()); - Flags |= ImGuiWindowFlags.NoDocking; - - SizeConstraints = new WindowSizeConstraints() - { - MinimumSize = new Vector2(375, 400), - MaximumSize = new Vector2(375, 2000), - }; _characterAnalyzer = characterAnalyzer; _playerPerformanceConfig = playerPerformanceConfig; _lightlessMediator = mediator; } + public override void OnClose() + { + ForceReleaseFocus(); + base.OnClose(); + } + protected override void DrawInternal() { + var drawList = ImGui.GetWindowDrawList(); + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize); + _windowContentWidth = UiSharedService.GetWindowContentRegionWidth(); if (!_apiController.IsCurrentVersion) { @@ -223,29 +210,49 @@ public class CompactUi : WindowMediatorSubscriberBase using (ImRaii.PushId("header")) DrawUIDHeader(); _uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f); - using (ImRaii.PushId("serverstatus")) DrawServerStatus(); + using (ImRaii.PushId("serverstatus")) + { + DrawServerStatus(); + } + selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime); + var style = ImGui.GetStyle(); + var contentMinY = windowPos.Y + ImGui.GetWindowContentRegionMin().Y; + var gradientInset = 4f * ImGuiHelpers.GlobalScale; + var gradientTop = MathF.Max(contentMinY, ImGui.GetCursorScreenPos().Y - style.ItemSpacing.Y + gradientInset); ImGui.Separator(); if (_apiController.ServerState is ServerState.Connected) { - using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(); + var pairSnapshot = _pairUiService.GetSnapshot(); + + using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(pairSnapshot); using (ImRaii.PushId("pairlist")) DrawPairs(); ImGui.Separator(); + var transfersTop = ImGui.GetCursorScreenPos().Y; + var gradientBottom = MathF.Max(gradientTop, transfersTop - style.ItemSpacing.Y - gradientInset); + selune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime); float pairlistEnd = ImGui.GetCursorPosY(); using (ImRaii.PushId("transfers")) DrawTransfers(); _transferPartHeight = ImGui.GetCursorPosY() - pairlistEnd - ImGui.GetTextLineHeight(); - using (ImRaii.PushId("group-pair-popup")) _selectPairsForGroupUi.Draw(_pairManager.DirectPairs); - using (ImRaii.PushId("group-syncshell-popup")) _selectSyncshellForTagUi.Draw([.. _pairManager.Groups.Values]); + using (ImRaii.PushId("group-pair-popup")) _selectPairsForGroupUi.Draw(pairSnapshot.DirectPairs); + using (ImRaii.PushId("group-syncshell-popup")) _selectSyncshellForTagUi.Draw(pairSnapshot.Groups); using (ImRaii.PushId("group-pair-edit")) _renamePairTagUi.Draw(); using (ImRaii.PushId("group-syncshell-edit")) _renameSyncshellTagUi.Draw(); using (ImRaii.PushId("grouping-pair-popup")) _selectTagForPairUi.Draw(); using (ImRaii.PushId("grouping-syncshell-popup")) _selectTagForSyncshellUi.Draw(); } - - if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null) + else { - _lastAddedUser = _pairManager.LastAddedUser; - _pairManager.LastAddedUser = null; + selune.Animate(ImGui.GetIO().DeltaTime); + } + + ProcessFocusTracker(); + + var lastAddedPair = _pairUiService.GetLastAddedPair(); + if (_configService.Current.OpenPopupOnAdd && lastAddedPair is not null) + { + _lastAddedUser = lastAddedPair; + _pairUiService.ClearLastAddedPair(); ImGui.OpenPopup("Set Notes for New User"); _showModalForUserAddition = true; _lastAddedUserComment = string.Empty; @@ -285,20 +292,22 @@ public class CompactUi : WindowMediatorSubscriberBase private void DrawPairs() { - var ySize = _transferPartHeight == 0 + float ySize = Math.Abs(_transferPartHeight) < 0.0001f ? 1 : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y + ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY(); - ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false); - - foreach (var item in _drawFolders) + if (ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false)) { - item.Draw(); + foreach (var item in _drawFolders) + { + item.Draw(); + } } ImGui.EndChild(); } + private void DrawServerStatus() { var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link); @@ -371,6 +380,19 @@ public class CompactUi : WindowMediatorSubscriberBase } } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive()) + { + Selune.RegisterHighlight( + ImGui.GetItemRectMin(), + ImGui.GetItemRectMax(), + SeluneHighlightMode.Both, + borderOnly: true, + borderThicknessOverride: _connectButtonHighlightThickness, + exactSize: true, + clipToElement: true, + roundingOverride: ImGui.GetStyle().FrameRounding); + } + UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName); } } @@ -470,6 +492,7 @@ public class CompactUi : WindowMediatorSubscriberBase return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes); } + [StructLayout(LayoutKind.Auto)] private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes) { public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0; @@ -477,7 +500,8 @@ public class CompactUi : WindowMediatorSubscriberBase private void DrawUIDHeader() { - var uidText = GetUidText(); + var uidText = _apiController.ServerState.GetUidText(_apiController.DisplayName); + var uidColor = _apiController.ServerState.GetUidColor(); Vector4? vanityTextColor = null; Vector4? vanityGlowColor = null; @@ -527,6 +551,17 @@ public class CompactUi : WindowMediatorSubscriberBase if (ImGui.IsItemHovered()) { + var padding = new Vector2(10f * ImGuiHelpers.GlobalScale); + Selune.RegisterHighlight( + ImGui.GetItemRectMin() - padding, + ImGui.GetItemRectMax() + padding, + SeluneHighlightMode.Point, + exactSize: true, + clipToElement: true, + clipPadding: padding, + highlightColorOverride: UIColors.Get("LightlessGreen"), + highlightAlphaOverride: 0.2f); + ImGui.BeginTooltip(); ImGui.PushTextWrapPos(ImGui.GetFontSize() * 32f); @@ -539,7 +574,7 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.PopStyleColor(); ImGuiHelpers.ScaledDummy(0.2f); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); if (_configService.Current.BroadcastEnabled) { @@ -581,7 +616,7 @@ public class CompactUi : WindowMediatorSubscriberBase } if (ImGui.IsItemClicked()) - _lightlessMediator.Publish(new UiToggleMessage(typeof(BroadcastUI))); + _lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI))); } ImGui.SetCursorPosY(cursorY); @@ -594,15 +629,30 @@ public class CompactUi : WindowMediatorSubscriberBase { var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor); var cursorPos = ImGui.GetCursorScreenPos(); - var fontPtr = ImGui.GetFont(); - SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr, "uid-header"); + var targetFontSize = ImGui.GetFontSize(); + var font = ImGui.GetFont(); + SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize ,font , "uid-header"); } else { - ImGui.TextColored(GetUidColor(), uidText); + ImGui.TextColored(uidColor, uidText); } } + if (ImGui.IsItemHovered()) + { + var padding = new Vector2(35f * ImGuiHelpers.GlobalScale); + Selune.RegisterHighlight( + ImGui.GetItemRectMin() - padding, + ImGui.GetItemRectMax() + padding, + SeluneHighlightMode.Point, + exactSize: true, + clipToElement: true, + clipPadding: padding, + highlightColorOverride: vanityGlowColor, + highlightAlphaOverride: 0.05f); + } + headerItemClicked = ImGui.IsItemClicked(); if (headerItemClicked) @@ -667,12 +717,27 @@ public class CompactUi : WindowMediatorSubscriberBase { var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.UID, vanityTextColor, vanityGlowColor); var cursorPos = ImGui.GetCursorScreenPos(); - var fontPtr = ImGui.GetFont(); - SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr, "uid-footer"); + var targetFontSize = ImGui.GetFontSize(); + var font = ImGui.GetFont(); + SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize, font, "uid-footer"); } else { - ImGui.TextColored(GetUidColor(), _apiController.UID); + ImGui.TextColored(uidColor, _apiController.UID); + } + + if (ImGui.IsItemHovered()) + { + var padding = new Vector2(30f * ImGuiHelpers.GlobalScale); + Selune.RegisterHighlight( + ImGui.GetItemRectMin() - padding, + ImGui.GetItemRectMax() + padding, + SeluneHighlightMode.Point, + exactSize: true, + clipToElement: true, + clipPadding: padding, + highlightColorOverride: vanityGlowColor, + highlightAlphaOverride: 0.05f); } bool uidFooterClicked = ImGui.IsItemClicked(); @@ -685,7 +750,7 @@ public class CompactUi : WindowMediatorSubscriberBase } else { - UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor()); + UiSharedService.ColorTextWrapped(_apiController.ServerState.GetServerError(_apiController.AuthFailureMessage), uidColor); } } @@ -696,28 +761,45 @@ public class CompactUi : WindowMediatorSubscriberBase var drawFolders = new List(); var filter = _tabMenu.Filter; - var allPairs = _pairManager.PairsWithGroups.ToDictionary(k => k.Key, k => k.Value); - var filteredPairs = allPairs.Where(p => PassesFilter(p.Key, filter)).ToDictionary(k => k.Key, k => k.Value); + var allEntries = _drawEntityFactory.GetAllEntries().ToList(); + var filteredEntries = string.IsNullOrEmpty(filter) + ? allEntries + : allEntries.Where(e => PassesFilter(e, filter)).ToList(); + + var syncshells = _pairLedger.GetAllSyncshells(); + var groupInfos = syncshells.Values + .Select(s => s.GroupFullInfo) + .OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase) + .ToList(); + + var entryLookup = allEntries.ToDictionary(e => e.DisplayEntry.Ident.UserId, StringComparer.Ordinal); + var filteredEntryLookup = filteredEntries.ToDictionary(e => e.DisplayEntry.Ident.UserId, StringComparer.Ordinal); //Filter of online/visible pairs if (_configService.Current.ShowVisibleUsersSeparately) { - var allVisiblePairs = ImmutablePairList(allPairs.Where(p => FilterVisibleUsers(p.Key))); - var filteredVisiblePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterVisibleUsers(p.Key))); - - drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs)); + var allVisiblePairs = SortVisibleEntries(allEntries.Where(FilterVisibleUsers)); + if (allVisiblePairs.Count > 0) + { + var filteredVisiblePairs = SortVisibleEntries(filteredEntries.Where(FilterVisibleUsers)); + drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs)); + } } //Filter of not foldered syncshells var groupFolders = new List(); - foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)) + foreach (var group in groupInfos) { - GetGroups(allPairs, filteredPairs, group, out ImmutableList allGroupPairs, out Dictionary> filteredGroupPairs); - - if (FilterNotTaggedSyncshells(group)) + if (!FilterNotTaggedSyncshells(group)) { - groupFolders.Add(new GroupFolder(group, _drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs))); + continue; } + + var allGroupEntries = ResolveGroupEntries(entryLookup, syncshells, group, applyFilters: false); + var filteredGroupEntries = ResolveGroupEntries(filteredEntryLookup, syncshells, group, applyFilters: true); + // Always create the folder so empty syncshells remain visible in the UI. + var drawGroupFolder = _drawEntityFactory.CreateGroupFolder(group.Group.GID, group, filteredGroupEntries, allGroupEntries); + groupFolders.Add(new GroupFolder(group, drawGroupFolder)); } //Filter of grouped up syncshells (All Syncshells Folder) @@ -730,190 +812,224 @@ public class CompactUi : WindowMediatorSubscriberBase //Filter of grouped/foldered pairs foreach (var tag in _tagHandler.GetAllPairTagsSorted()) { - var allTagPairs = ImmutablePairList(allPairs.Where(p => FilterTagUsers(p.Key, tag))); - var filteredTagPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterTagUsers(p.Key, tag) && FilterOnlineOrPausedSelf(p.Key))); - - drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs)); + var allTagPairs = SortEntries(allEntries.Where(e => FilterTagUsers(e, tag))); + if (allTagPairs.Count > 0) + { + var filteredTagPairs = SortEntries(filteredEntries.Where(e => FilterTagUsers(e, tag) && FilterOnlineOrPausedSelf(e))); + drawFolders.Add(_drawEntityFactory.CreateTagFolder(tag, filteredTagPairs, allTagPairs)); + } } //Filter of grouped/foldered syncshells foreach (var syncshellTag in _tagHandler.GetAllSyncshellTagsSorted()) { var syncshellFolderTags = new List(); - foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)) + foreach (var group in groupInfos) { - if (_tagHandler.HasSyncshellTag(group.GID, syncshellTag)) + if (!_tagHandler.HasSyncshellTag(group.Group.GID, syncshellTag)) { - GetGroups(allPairs, filteredPairs, group, - out ImmutableList allGroupPairs, - out Dictionary> filteredGroupPairs); - - syncshellFolderTags.Add(new GroupFolder(group, _drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs))); + continue; } + + var allGroupEntries = ResolveGroupEntries(entryLookup, syncshells, group, applyFilters: false); + var filteredGroupEntries = ResolveGroupEntries(filteredEntryLookup, syncshells, group, applyFilters: true); + // Keep tagged syncshells rendered regardless of whether membership info has loaded. + var taggedGroupFolder = _drawEntityFactory.CreateGroupFolder($"tag_{group.Group.GID}", group, filteredGroupEntries, allGroupEntries); + syncshellFolderTags.Add(new GroupFolder(group, taggedGroupFolder)); } drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _apiController, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshellTag)); } //Filter of not grouped/foldered and offline pairs - var allOnlineNotTaggedPairs = ImmutablePairList(allPairs.Where(p => FilterNotTaggedUsers(p.Key))); - var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterNotTaggedUsers(p.Key) && FilterOnlineOrPausedSelf(p.Key))); - - drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag), onlineNotTaggedPairs, allOnlineNotTaggedPairs)); + var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers)); + if (allOnlineNotTaggedPairs.Count > 0 && _configService.Current.ShowOfflineUsersSeparately) { + var filteredOnlineEntries = SortOnlineEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e))); + drawFolders.Add(_drawEntityFactory.CreateTagFolder( + TagHandler.CustomOnlineTag, + filteredOnlineEntries, + allOnlineNotTaggedPairs)); + } else if (allOnlineNotTaggedPairs.Count > 0 && !_configService.Current.ShowOfflineUsersSeparately) { + var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(FilterNotTaggedUsers)); + drawFolders.Add(_drawEntityFactory.CreateTagFolder( + TagHandler.CustomAllTag, + onlineNotTaggedPairs, + allOnlineNotTaggedPairs)); + } if (_configService.Current.ShowOfflineUsersSeparately) { - var allOfflinePairs = ImmutablePairList(allPairs.Where(p => FilterOfflineUsers(p.Key, p.Value))); - var filteredOfflinePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineUsers(p.Key, p.Value))); - - drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs)); + var allOfflinePairs = SortEntries(allEntries.Where(FilterOfflineUsers)); + if (allOfflinePairs.Count > 0) + { + var filteredOfflinePairs = SortEntries(filteredEntries.Where(FilterOfflineUsers)); + drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs)); + } if (_configService.Current.ShowSyncshellOfflineUsersSeparately) { - var allOfflineSyncshellUsers = ImmutablePairList(allPairs.Where(p => FilterOfflineSyncshellUsers(p.Key))); - var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineSyncshellUsers(p.Key))); - - drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers)); + var allOfflineSyncshellUsers = SortEntries(allEntries.Where(FilterOfflineSyncshellUsers)); + if (allOfflineSyncshellUsers.Count > 0) + { + var filteredOfflineSyncshellUsers = SortEntries(filteredEntries.Where(FilterOfflineSyncshellUsers)); + drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers)); + } } } - //Unpaired - drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag, - BasicSortedDictionary(filteredPairs.Where(p => p.Key.IsOneSidedPair)), - ImmutablePairList(allPairs.Where(p => p.Key.IsOneSidedPair)))); + //Unpaired + var unpairedAllEntries = SortEntries(allEntries.Where(e => e.IsOneSided)); + if (unpairedAllEntries.Count > 0) + { + var unpairedFiltered = SortEntries(filteredEntries.Where(e => e.IsOneSided)); + drawFolders.Add(_drawEntityFactory.CreateTagFolder(TagHandler.CustomUnpairedTag, unpairedFiltered, unpairedAllEntries)); + } return drawFolders; } } - private static bool PassesFilter(Pair pair, string filter) + private static bool PassesFilter(PairUiEntry entry, string filter) { if (string.IsNullOrEmpty(filter)) return true; - return pair.UserData.AliasOrUID.Contains(filter, StringComparison.OrdinalIgnoreCase) || (pair.GetNote()?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false) || (pair.PlayerName?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false); + return entry.AliasOrUid.Contains(filter, StringComparison.OrdinalIgnoreCase) + || (!string.IsNullOrEmpty(entry.Note) && entry.Note.Contains(filter, StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrEmpty(entry.DisplayName) && entry.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)); } - private string AlphabeticalSortKey(Pair pair) + private string AlphabeticalSortKey(PairUiEntry entry) { - if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(pair.PlayerName)) + if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(entry.DisplayName)) { - return _configService.Current.PreferNotesOverNamesForVisible ? (pair.GetNote() ?? string.Empty) : pair.PlayerName; + return _configService.Current.PreferNotesOverNamesForVisible ? (entry.Note ?? string.Empty) : entry.DisplayName; } - return pair.GetNote() ?? pair.UserData.AliasOrUID; + return !string.IsNullOrEmpty(entry.Note) ? entry.Note : entry.AliasOrUid; } - private bool FilterOnlineOrPausedSelf(Pair pair) => pair.IsOnline || (!pair.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) || pair.UserPair.OwnPermissions.IsPaused(); + private bool FilterOnlineOrPausedSelf(PairUiEntry entry) => entry.IsOnline || (!entry.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) || entry.SelfPermissions.IsPaused(); - private bool FilterVisibleUsers(Pair pair) => pair.IsVisible && (_configService.Current.ShowSyncshellUsersInVisible || pair.IsDirectlyPaired); + private bool FilterVisibleUsers(PairUiEntry entry) => entry.IsVisible && entry.IsOnline && (_configService.Current.ShowSyncshellUsersInVisible || entry.IsDirectlyPaired); - private bool FilterTagUsers(Pair pair, string tag) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && _tagHandler.HasPairTag(pair.UserData.UID, tag); + private bool FilterTagUsers(PairUiEntry entry, string tag) => entry.IsDirectlyPaired && !entry.IsOneSided && _tagHandler.HasPairTag(entry.DisplayEntry.Ident.UserId, tag); - private static bool FilterGroupUsers(List groups, GroupFullInfoDto group) => groups.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal)); - - private bool FilterNotTaggedUsers(Pair pair) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && !_tagHandler.HasAnyPairTag(pair.UserData.UID); + private bool FilterNotTaggedUsers(PairUiEntry entry) => entry.IsDirectlyPaired && !entry.IsOneSided && !_tagHandler.HasAnyPairTag(entry.DisplayEntry.Ident.UserId); private bool FilterNotTaggedSyncshells(GroupFullInfoDto group) => !_tagHandler.HasAnySyncshellTag(group.GID) || _configService.Current.ShowGroupedSyncshellsInAll; - private bool FilterOfflineUsers(Pair pair, List groups) => ((pair.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately) || !_configService.Current.ShowSyncshellOfflineUsersSeparately) && (!pair.IsOneSidedPair || groups.Count != 0) && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused(); - - private static bool FilterOfflineSyncshellUsers(Pair pair) => !pair.IsDirectlyPaired && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused(); - - private Dictionary> BasicSortedDictionary(IEnumerable>> pairs) => pairs.OrderByDescending(u => u.Key.IsVisible).ThenByDescending(u => u.Key.IsOnline).ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase).ToDictionary(u => u.Key, u => u.Value); - - private static ImmutableList ImmutablePairList(IEnumerable>> pairs) => [.. pairs.Select(k => k.Key)]; - - private void GetGroups(Dictionary> allPairs, - Dictionary> filteredPairs, - GroupFullInfoDto group, - out ImmutableList allGroupPairs, - out Dictionary> filteredGroupPairs) + private bool FilterOfflineUsers(PairUiEntry entry) { - allGroupPairs = ImmutablePairList(allPairs - .Where(u => FilterGroupUsers(u.Value, group))); - - filteredGroupPairs = filteredPairs - .Where(u => FilterGroupUsers(u.Value, group) && FilterOnlineOrPausedSelf(u.Key)) - .OrderByDescending(u => u.Key.IsOnline) - .ThenBy(u => - { - if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0; - if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info)) - { - if (info.IsModerator()) return 1; - if (info.IsPinned()) return 2; - } - return u.Key.IsVisible ? 3 : 4; - }) - .ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase) - .ToDictionary(k => k.Key, k => k.Value); + var groups = entry.DisplayEntry.Groups; + var includeDirect = _configService.Current.ShowSyncshellOfflineUsersSeparately ? entry.IsDirectlyPaired : true; + var includeGroup = !entry.IsOneSided || groups.Count != 0; + return includeDirect && includeGroup && !entry.IsOnline && !entry.SelfPermissions.IsPaused(); } - private string GetServerError() + private static bool FilterOfflineSyncshellUsers(PairUiEntry entry) => !entry.IsDirectlyPaired && !entry.IsOnline && !entry.SelfPermissions.IsPaused(); + + private ImmutableList SortEntries(IEnumerable entries) { - return _apiController.ServerState switch + return [.. entries + .OrderByDescending(e => e.IsVisible) + .ThenByDescending(e => e.IsOnline) + .ThenBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)]; + } + + private ImmutableList SortVisibleEntries(IEnumerable entries) + { + var entryList = entries.ToList(); + return _configService.Current.VisiblePairSortMode switch { - ServerState.Connecting => "Attempting to connect to the server.", - ServerState.Reconnecting => "Connection to server interrupted, attempting to reconnect to the server.", - ServerState.Disconnected => "You are currently disconnected from the Lightless Sync server.", - ServerState.Disconnecting => "Disconnecting from the server", - ServerState.Unauthorized => "Server Response: " + _apiController.AuthFailureMessage, - ServerState.Offline => "Your selected Lightless Sync server is currently offline.", - ServerState.VersionMisMatch => - "Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.", - ServerState.RateLimited => "You are rate limited for (re)connecting too often. Disconnect, wait 10 minutes and try again.", - ServerState.Connected => string.Empty, - ServerState.NoSecretKey => "You have no secret key set for this current character. Open Settings -> Service Settings and set a secret key for the current character. You can reuse the same secret key for multiple characters.", - ServerState.MultiChara => "Your Character Configuration has multiple characters configured with same name and world. You will not be able to connect until you fix this issue. Remove the duplicates from the configuration in Settings -> Service Settings -> Character Management and reconnect manually after.", - ServerState.OAuthMisconfigured => "OAuth2 is enabled but not fully configured, verify in the Settings -> Service Settings that you have OAuth2 connected and, importantly, a UID assigned to your current character.", - ServerState.OAuthLoginTokenStale => "Your OAuth2 login token is stale and cannot be used to renew. Go to the Settings -> Service Settings and unlink then relink your OAuth2 configuration.", - ServerState.NoAutoLogon => "This character has automatic login into Lightless disabled. Press the connect button to connect to Lightless.", - _ => string.Empty + VisiblePairSortMode.VramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateVramBytes), + VisiblePairSortMode.EffectiveVramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveVramBytes), + VisiblePairSortMode.TriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedDataTris), + VisiblePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)], + VisiblePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList), + _ => SortEntries(entryList), }; } - private Vector4 GetUidColor() + private ImmutableList SortOnlineEntries(IEnumerable entries) { - return _apiController.ServerState switch + var entryList = entries.ToList(); + return _configService.Current.OnlinePairSortMode switch { - ServerState.Connecting => UIColors.Get("LightlessYellow"), - ServerState.Reconnecting => UIColors.Get("DimRed"), - ServerState.Connected => UIColors.Get("LightlessPurple"), - ServerState.Disconnected => UIColors.Get("LightlessYellow"), - ServerState.Disconnecting => UIColors.Get("LightlessYellow"), - ServerState.Unauthorized => UIColors.Get("DimRed"), - ServerState.VersionMisMatch => UIColors.Get("DimRed"), - ServerState.Offline => UIColors.Get("DimRed"), - ServerState.RateLimited => UIColors.Get("LightlessYellow"), - ServerState.NoSecretKey => UIColors.Get("LightlessYellow"), - ServerState.MultiChara => UIColors.Get("LightlessYellow"), - ServerState.OAuthMisconfigured => UIColors.Get("DimRed"), - ServerState.OAuthLoginTokenStale => UIColors.Get("DimRed"), - ServerState.NoAutoLogon => UIColors.Get("LightlessYellow"), - _ => UIColors.Get("DimRed") + OnlinePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)], + OnlinePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList), + _ => SortEntries(entryList), }; } - private string GetUidText() + private ImmutableList SortVisibleByMetric(IEnumerable entries, Func selector) { - return _apiController.ServerState switch + return [.. entries + .OrderByDescending(entry => selector(entry) >= 0) + .ThenByDescending(selector) + .ThenByDescending(entry => entry.IsOnline) + .ThenBy(entry => AlphabeticalSortKey(entry), StringComparer.OrdinalIgnoreCase)]; + } + + private ImmutableList SortVisibleByPreferred(IEnumerable entries) + { + return [.. entries + .OrderByDescending(entry => entry.IsDirectlyPaired && entry.SelfPermissions.IsSticky()) + .ThenByDescending(entry => entry.IsDirectlyPaired) + .ThenByDescending(entry => entry.IsOnline) + .ThenBy(entry => AlphabeticalSortKey(entry), StringComparer.OrdinalIgnoreCase)]; + } + + private ImmutableList SortGroupEntries(IEnumerable entries, GroupFullInfoDto group) + { + return [.. entries + .OrderByDescending(e => e.IsOnline) + .ThenBy(e => GroupSortWeight(e, group)) + .ThenBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)]; + } + + private int GroupSortWeight(PairUiEntry entry, GroupFullInfoDto group) + { + if (string.Equals(entry.DisplayEntry.Ident.UserId, group.OwnerUID, StringComparison.Ordinal)) { - ServerState.Reconnecting => "Reconnecting", - ServerState.Connecting => "Connecting", - ServerState.Disconnected => "Disconnected", - ServerState.Disconnecting => "Disconnecting", - ServerState.Unauthorized => "Unauthorized", - ServerState.VersionMisMatch => "Version mismatch", - ServerState.Offline => "Unavailable", - ServerState.RateLimited => "Rate Limited", - ServerState.NoSecretKey => "No Secret Key", - ServerState.MultiChara => "Duplicate Characters", - ServerState.OAuthMisconfigured => "Misconfigured OAuth2", - ServerState.OAuthLoginTokenStale => "Stale OAuth2", - ServerState.NoAutoLogon => "Auto Login disabled", - ServerState.Connected => _apiController.DisplayName, - _ => string.Empty - }; + return 0; + } + + if (group.GroupPairUserInfos.TryGetValue(entry.DisplayEntry.Ident.UserId, out var info)) + { + if (info.IsModerator()) return 1; + if (info.IsPinned()) return 2; + } + + return entry.IsVisible ? 3 : 4; + } + + private ImmutableList ResolveGroupEntries( + IReadOnlyDictionary entryLookup, + IReadOnlyDictionary syncshells, + GroupFullInfoDto group, + bool applyFilters) + { + if (!syncshells.TryGetValue(group.Group.GID, out var shell)) + { + return ImmutableList.Empty; + } + + var entries = shell.Users.Keys + .Select(id => entryLookup.TryGetValue(id, out var entry) ? entry : null) + .Where(entry => entry is not null) + .Cast(); + + if (applyFilters && _configService.Current.ShowOfflineUsersSeparately) + { + entries = entries.Where(entry => !FilterOfflineUsers(entry)); + } + + if (applyFilters && _configService.Current.ShowSyncshellOfflineUsersSeparately) + { + entries = entries.Where(entry => !FilterOfflineSyncshellUsers(entry)); + } + + return SortGroupEntries(entries, group); } private void UiSharedService_GposeEnd() @@ -926,4 +1042,50 @@ public class CompactUi : WindowMediatorSubscriberBase _wasOpen = IsOpen; IsOpen = false; } + + private void RegisterFocusCharacter(Pair pair) + { + _pendingFocusPair = pair; + _pendingFocusFrame = ImGui.GetFrameCount(); + } + + private void ProcessFocusTracker() + { + var frame = ImGui.GetFrameCount(); + Pair? character = _pendingFocusFrame == frame ? _pendingFocusPair : null; + if (!ReferenceEquals(character, _focusedPair)) + { + if (character is null) + { + _dalamudUtilService.ReleaseVisiblePairFocus(); + } + else + { + _dalamudUtilService.FocusVisiblePair(character); + } + + _focusedPair = character; + } + + if (_pendingFocusFrame == frame) + { + _pendingFocusPair = null; + _pendingFocusFrame = -1; + } + } + + private void ForceReleaseFocus() + { + if (_focusedPair is null) + { + _pendingFocusPair = null; + _pendingFocusFrame = -1; + return; + } + + _dalamudUtilService.ReleaseVisiblePairFocus(); + _focusedPair = null; + _pendingFocusPair = null; + _pendingFocusFrame = -1; + } } diff --git a/LightlessSync/UI/Components/DrawFolderBase.cs b/LightlessSync/UI/Components/DrawFolderBase.cs index 15d558e..0532da9 100644 --- a/LightlessSync/UI/Components/DrawFolderBase.cs +++ b/LightlessSync/UI/Components/DrawFolderBase.cs @@ -1,9 +1,11 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using LightlessSync.PlayerData.Pairs; using LightlessSync.UI.Handlers; +using LightlessSync.UI.Models; using System.Collections.Immutable; +using LightlessSync.UI.Style; +using OtterGui.Text; namespace LightlessSync.UI.Components; @@ -11,16 +13,18 @@ public abstract class DrawFolderBase : IDrawFolder { public IImmutableList DrawPairs { get; init; } protected readonly string _id; - protected readonly IImmutableList _allPairs; + protected readonly IImmutableList _allPairs; protected readonly TagHandler _tagHandler; protected readonly UiSharedService _uiSharedService; private float _menuWidth = -1; - public int OnlinePairs => DrawPairs.Count(u => u.Pair.IsOnline); + public int OnlinePairs => DrawPairs.Count(u => u.DisplayEntry.Connection.IsOnline); public int TotalPairs => _allPairs.Count; private bool _wasHovered = false; + private bool _suppressNextRowToggle; + private bool _rowClickArmed; protected DrawFolderBase(string id, IImmutableList drawPairs, - IImmutableList allPairs, TagHandler tagHandler, UiSharedService uiSharedService) + IImmutableList allPairs, TagHandler tagHandler, UiSharedService uiSharedService) { _id = id; DrawPairs = drawPairs; @@ -31,11 +35,14 @@ public abstract class DrawFolderBase : IDrawFolder protected abstract bool RenderIfEmpty { get; } protected abstract bool RenderMenu { get; } + protected virtual bool EnableRowClick => true; public void Draw() { if (!RenderIfEmpty && !DrawPairs.Any()) return; + _suppressNextRowToggle = false; + using var id = ImRaii.PushId("folder_" + _id); var color = ImRaii.PushColor(ImGuiCol.ChildBg, ImGui.GetColorU32(ImGuiCol.FrameBgHovered), _wasHovered); using (ImRaii.Child("folder__" + _id, new System.Numerics.Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight()))) @@ -48,7 +55,8 @@ public abstract class DrawFolderBase : IDrawFolder _uiSharedService.IconText(icon); if (ImGui.IsItemClicked()) { - _tagHandler.SetTagOpen(_id, !_tagHandler.IsTagOpen(_id)); + ToggleFolderOpen(); + SuppressNextRowToggle(); } ImGui.SameLine(); @@ -62,10 +70,41 @@ public abstract class DrawFolderBase : IDrawFolder DrawName(rightSideStart - leftSideEnd); } - _wasHovered = ImGui.IsItemHovered(); + var rowHovered = ImGui.IsItemHovered(); + _wasHovered = rowHovered; + + if (EnableRowClick) + { + if (rowHovered && ImGui.IsMouseClicked(ImGuiMouseButton.Left) && !_suppressNextRowToggle) + { + _rowClickArmed = true; + } + + if (_rowClickArmed && rowHovered && ImGui.IsMouseReleased(ImGuiMouseButton.Left)) + { + ToggleFolderOpen(); + _rowClickArmed = false; + } + + if (!ImGui.IsMouseDown(ImGuiMouseButton.Left)) + { + _rowClickArmed = false; + } + } + else + { + _rowClickArmed = false; + } + + if (_wasHovered) + { + Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), spanFullWidth: true); + } color.Dispose(); + _suppressNextRowToggle = false; + ImGui.Separator(); // if opened draw content @@ -74,9 +113,13 @@ public abstract class DrawFolderBase : IDrawFolder using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false); if (DrawPairs.Any()) { - foreach (var item in DrawPairs) + using var clipper = ImUtf8.ListClipper(DrawPairs.Count, ImGui.GetFrameHeightWithSpacing()); + while (clipper.Step()) { - item.DrawPairedClient(); + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + DrawPairs[i].DrawPairedClient(); + } } } else @@ -110,6 +153,7 @@ public abstract class DrawFolderBase : IDrawFolder ImGui.SameLine(windowEndX - barButtonSize.X); if (_uiSharedService.IconButton(FontAwesomeIcon.EllipsisV)) { + SuppressNextRowToggle(); ImGui.OpenPopup("User Flyout Menu"); } if (ImGui.BeginPopup("User Flyout Menu")) @@ -123,7 +167,16 @@ public abstract class DrawFolderBase : IDrawFolder _menuWidth = 0; } } - return DrawRightSide(rightSideStart); } + + protected void SuppressNextRowToggle() + { + _suppressNextRowToggle = true; + } + + private void ToggleFolderOpen() + { + _tagHandler.SetTagOpen(_id, !_tagHandler.IsTagOpen(_id)); + } } \ No newline at end of file diff --git a/LightlessSync/UI/Components/DrawFolderGroup.cs b/LightlessSync/UI/Components/DrawFolderGroup.cs index 6de9e28..c39326c 100644 --- a/LightlessSync/UI/Components/DrawFolderGroup.cs +++ b/LightlessSync/UI/Components/DrawFolderGroup.cs @@ -5,9 +5,9 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; -using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; using LightlessSync.UI.Handlers; +using LightlessSync.UI.Models; using LightlessSync.WebAPI; using System.Collections.Immutable; @@ -22,7 +22,7 @@ public class DrawFolderGroup : DrawFolderBase private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi; public DrawFolderGroup(string id, GroupFullInfoDto groupFullInfoDto, ApiController apiController, - IImmutableList drawPairs, IImmutableList allPairs, TagHandler tagHandler, IdDisplayHandler idDisplayHandler, + IImmutableList drawPairs, IImmutableList allPairs, TagHandler tagHandler, IdDisplayHandler idDisplayHandler, LightlessMediator lightlessMediator, UiSharedService uiSharedService, SelectTagForSyncshellUi selectTagForSyncshellUi) : base(id, drawPairs, allPairs, tagHandler, uiSharedService) { @@ -35,6 +35,7 @@ public class DrawFolderGroup : DrawFolderBase protected override bool RenderIfEmpty => true; protected override bool RenderMenu => true; + protected override bool EnableRowClick => false; private bool IsModerator => IsOwner || _groupFullInfoDto.GroupUserInfo.IsModerator(); private bool IsOwner => string.Equals(_groupFullInfoDto.OwnerUID, _apiController.UID, StringComparison.Ordinal); private bool IsPinned => _groupFullInfoDto.GroupUserInfo.IsPinned(); @@ -87,6 +88,13 @@ public class DrawFolderGroup : DrawFolderBase ImGui.Separator(); ImGui.TextUnformatted("General Syncshell Actions"); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.AddressCard, "Open Syncshell Profile", menuWidth, true)) + { + ImGui.CloseCurrentPopup(); + _lightlessMediator.Publish(new GroupProfileOpenStandaloneMessage(_groupFullInfoDto.Group)); + } + UiSharedService.AttachToolTip("Opens the profile for this syncshell in a new window."); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy ID", menuWidth, true)) { ImGui.CloseCurrentPopup(); @@ -111,6 +119,7 @@ public class DrawFolderGroup : DrawFolderBase if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell", menuWidth, true) && UiSharedService.CtrlPressed()) { _ = _apiController.GroupLeave(_groupFullInfoDto); + _lightlessMediator.Publish(new UserLeftSyncshell(_groupFullInfoDto.GID)); ImGui.CloseCurrentPopup(); } UiSharedService.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(_groupFullInfoDto.OwnerUID, _apiController.UID, StringComparison.Ordinal) @@ -160,6 +169,14 @@ public class DrawFolderGroup : DrawFolderBase { ImGui.Separator(); ImGui.TextUnformatted("Syncshell Admin Functions"); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserEdit, "Open Profile Editor", menuWidth, true)) + { + ImGui.CloseCurrentPopup(); + _lightlessMediator.Publish(new OpenGroupProfileEditorMessage(_groupFullInfoDto)); + } + UiSharedService.AttachToolTip("Open the syncshell profile editor."); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Cog, "Open Admin Panel", menuWidth, true)) { ImGui.CloseCurrentPopup(); @@ -244,6 +261,7 @@ public class DrawFolderGroup : DrawFolderBase ImGui.SameLine(); if (_uiSharedService.IconButton(pauseIcon)) { + SuppressNextRowToggle(); var perm = _groupFullInfoDto.GroupUserPermissions; perm.SetPaused(!perm.IsPaused()); _ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(_groupFullInfoDto.Group, new(_apiController.UID), perm)); diff --git a/LightlessSync/UI/Components/DrawFolderTag.cs b/LightlessSync/UI/Components/DrawFolderTag.cs index 0c114e1..b91617a 100644 --- a/LightlessSync/UI/Components/DrawFolderTag.cs +++ b/LightlessSync/UI/Components/DrawFolderTag.cs @@ -1,11 +1,18 @@ -using Dalamud.Bindings.ImGui; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; +using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; -using LightlessSync.PlayerData.Pairs; +using LightlessSync.LightlessConfiguration; +using LightlessSync.Services.Mediator; +using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Handlers; +using LightlessSync.UI.Models; using LightlessSync.WebAPI; -using System.Collections.Immutable; namespace LightlessSync.UI.Components; @@ -14,14 +21,30 @@ public class DrawFolderTag : DrawFolderBase private readonly ApiController _apiController; private readonly SelectPairForTagUi _selectPairForTagUi; private readonly RenamePairTagUi _renameTagUi; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly LightlessConfigService _configService; + private readonly LightlessMediator _mediator; - public DrawFolderTag(string id, IImmutableList drawPairs, IImmutableList allPairs, - TagHandler tagHandler, ApiController apiController, SelectPairForTagUi selectPairForTagUi, RenamePairTagUi renameTagUi, UiSharedService uiSharedService) + public DrawFolderTag( + string id, + IImmutableList drawPairs, + IImmutableList allPairs, + TagHandler tagHandler, + ApiController apiController, + SelectPairForTagUi selectPairForTagUi, + RenamePairTagUi renameTagUi, + UiSharedService uiSharedService, + ServerConfigurationManager serverConfigurationManager, + LightlessConfigService configService, + LightlessMediator mediator) : base(id, drawPairs, allPairs, tagHandler, uiSharedService) { _apiController = apiController; _selectPairForTagUi = selectPairForTagUi; _renameTagUi = renameTagUi; + _serverConfigurationManager = serverConfigurationManager; + _configService = configService; + _mediator = mediator; } protected override bool RenderIfEmpty => _id switch @@ -86,15 +109,18 @@ public class DrawFolderTag : DrawFolderBase if (RenderCount) { - using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f })) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemSpacing.X / 2f })) { ImGui.SameLine(); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("[" + OnlinePairs.ToString() + "]"); + ImGui.TextUnformatted($"[{OnlinePairs}]"); } - UiSharedService.AttachToolTip(OnlinePairs + " online" + Environment.NewLine + TotalPairs + " total"); + + UiSharedService.AttachToolTip($"{OnlinePairs} online{Environment.NewLine}{TotalPairs} total"); } + ImGui.SameLine(); return ImGui.GetCursorPosX(); } @@ -102,19 +128,24 @@ public class DrawFolderTag : DrawFolderBase protected override void DrawMenu(float menuWidth) { ImGui.TextUnformatted("Group Menu"); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Pairs", menuWidth, isInPopup: true)) + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Select Pairs", menuWidth, true)) { _selectPairForTagUi.Open(_id); } - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Edit, "Rename Pair Group", menuWidth, isInPopup: true)) + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Edit, "Rename Pair Group", menuWidth, true)) { _renameTagUi.Open(_id); } - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Pair Group", menuWidth, isInPopup: true) && UiSharedService.CtrlPressed()) + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Pair Group", menuWidth, true) && + UiSharedService.CtrlPressed()) { _tagHandler.RemovePairTag(_id); } - UiSharedService.AttachToolTip("Hold CTRL to remove this Group permanently." + Environment.NewLine + + + UiSharedService.AttachToolTip( + "Hold CTRL to remove this Group permanently." + Environment.NewLine + "Note: this will not unpair with users in this Group."); } @@ -122,7 +153,7 @@ public class DrawFolderTag : DrawFolderBase { ImGui.AlignTextToFramePadding(); - string name = _id switch + var name = _id switch { TagHandler.CustomUnpairedTag => "One-sided Individual Pairs", TagHandler.CustomOnlineTag => "Online / Paused by you", @@ -138,16 +169,30 @@ public class DrawFolderTag : DrawFolderBase protected override float DrawRightSide(float currentRightSideX) { - if (!RenderPause) return currentRightSideX; - - var allArePaused = _allPairs.All(pair => pair.UserPair!.OwnPermissions.IsPaused()); - var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; - var pauseButtonX = _uiSharedService.GetIconButtonSize(pauseButton).X; - - var buttonPauseOffset = currentRightSideX - pauseButtonX; - ImGui.SameLine(buttonPauseOffset); - if (_uiSharedService.IconButton(pauseButton)) + if (string.Equals(_id, TagHandler.CustomVisibleTag, StringComparison.Ordinal)) { + return DrawVisibleFilter(currentRightSideX); + } + + if (string.Equals(_id, TagHandler.CustomOnlineTag, StringComparison.Ordinal)) + { + return DrawOnlineFilter(currentRightSideX); + } + + if (!RenderPause) + { + return currentRightSideX; + } + + var allArePaused = _allPairs.All(entry => entry.SelfPermissions.IsPaused()); + var pauseIcon = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + var pauseButtonSize = _uiSharedService.GetIconButtonSize(pauseIcon); + + var buttonPauseOffset = currentRightSideX - pauseButtonSize.X; + ImGui.SameLine(buttonPauseOffset); + if (_uiSharedService.IconButton(pauseIcon)) + { + SuppressNextRowToggle(); if (allArePaused) { ResumeAllPairs(_allPairs); @@ -157,39 +202,138 @@ public class DrawFolderTag : DrawFolderBase PauseRemainingPairs(_allPairs); } } - if (allArePaused) - { - UiSharedService.AttachToolTip($"Resume pairing with all pairs in {_id}"); - } - else - { - UiSharedService.AttachToolTip($"Pause pairing with all pairs in {_id}"); - } + + UiSharedService.AttachToolTip(allArePaused + ? $"Resume pairing with all pairs in {_id}" + : $"Pause pairing with all pairs in {_id}"); return currentRightSideX; } - private void PauseRemainingPairs(IEnumerable availablePairs) + private void PauseRemainingPairs(IEnumerable entries) { - _ = _apiController.SetBulkPermissions(new(availablePairs - .ToDictionary(g => g.UserData.UID, g => - { - var perm = g.UserPair.OwnPermissions; - perm.SetPaused(paused: true); - return perm; - }, StringComparer.Ordinal), new(StringComparer.Ordinal))) + _ = _apiController.SetBulkPermissions(new( + entries.ToDictionary(entry => entry.DisplayEntry.User.UID, entry => + { + var permissions = entry.SelfPermissions; + permissions.SetPaused(true); + return permissions; + }, StringComparer.Ordinal), + new(StringComparer.Ordinal))) .ConfigureAwait(false); } - private void ResumeAllPairs(IEnumerable availablePairs) + private void ResumeAllPairs(IEnumerable entries) { - _ = _apiController.SetBulkPermissions(new(availablePairs - .ToDictionary(g => g.UserData.UID, g => - { - var perm = g.UserPair.OwnPermissions; - perm.SetPaused(paused: false); - return perm; - }, StringComparer.Ordinal), new(StringComparer.Ordinal))) + _ = _apiController.SetBulkPermissions(new( + entries.ToDictionary(entry => entry.DisplayEntry.User.UID, entry => + { + var permissions = entry.SelfPermissions; + permissions.SetPaused(false); + return permissions; + }, StringComparer.Ordinal), + new(StringComparer.Ordinal))) .ConfigureAwait(false); } + + private float DrawVisibleFilter(float currentRightSideX) + { + var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Filter); + var spacingX = ImGui.GetStyle().ItemSpacing.X; + var buttonStart = currentRightSideX - buttonSize.X; + + ImGui.SameLine(buttonStart); + if (_uiSharedService.IconButton(FontAwesomeIcon.Filter)) + { + SuppressNextRowToggle(); + ImGui.OpenPopup($"visible-filter-{_id}"); + } + + UiSharedService.AttachToolTip("Adjust how visible pairs are ordered."); + + if (ImGui.BeginPopup($"visible-filter-{_id}")) + { + ImGui.TextUnformatted("Visible Pair Ordering"); + ImGui.Separator(); + + foreach (VisiblePairSortMode mode in Enum.GetValues()) + { + var selected = _configService.Current.VisiblePairSortMode == mode; + if (ImGui.MenuItem(GetSortVisibleLabel(mode), string.Empty, selected)) + { + if (!selected) + { + _configService.Current.VisiblePairSortMode = mode; + _configService.Save(); + _mediator.Publish(new RefreshUiMessage()); + } + + ImGui.CloseCurrentPopup(); + } + } + + ImGui.EndPopup(); + } + + return buttonStart - spacingX; + } + + private float DrawOnlineFilter(float currentRightSideX) + { + var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Filter); + var spacingX = ImGui.GetStyle().ItemSpacing.X; + var buttonStart = currentRightSideX - buttonSize.X; + + ImGui.SameLine(buttonStart); + if (_uiSharedService.IconButton(FontAwesomeIcon.Filter)) + { + SuppressNextRowToggle(); + ImGui.OpenPopup($"online-filter-{_id}"); + } + + UiSharedService.AttachToolTip("Adjust how online pairs are ordered."); + + if (ImGui.BeginPopup($"online-filter-{_id}")) + { + ImGui.TextUnformatted("Online Pair Ordering"); + ImGui.Separator(); + + foreach (OnlinePairSortMode mode in Enum.GetValues()) + { + var selected = _configService.Current.OnlinePairSortMode == mode; + if (ImGui.MenuItem(GetSortOnlineLabel(mode), string.Empty, selected)) + { + if (!selected) + { + _configService.Current.OnlinePairSortMode = mode; + _configService.Save(); + _mediator.Publish(new RefreshUiMessage()); + } + + ImGui.CloseCurrentPopup(); + } + } + + ImGui.EndPopup(); + } + + return buttonStart - spacingX; + } + + private static string GetSortVisibleLabel(VisiblePairSortMode mode) => mode switch + { + VisiblePairSortMode.Alphabetical => "Alphabetical", + VisiblePairSortMode.VramUsage => "VRAM usage (descending)", + VisiblePairSortMode.EffectiveVramUsage => "Effective VRAM usage (descending)", + VisiblePairSortMode.TriangleCount => "Triangle count (descending)", + VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs", + _ => "Default", + }; + + private static string GetSortOnlineLabel(OnlinePairSortMode mode) => mode switch + { + OnlinePairSortMode.Alphabetical => "Alphabetical", + OnlinePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs", + _ => "Default", + }; } \ No newline at end of file diff --git a/LightlessSync/UI/Components/DrawGroupedGroupFolder.cs b/LightlessSync/UI/Components/DrawGroupedGroupFolder.cs index 1bb3d79..72063f2 100644 --- a/LightlessSync/UI/Components/DrawGroupedGroupFolder.cs +++ b/LightlessSync/UI/Components/DrawGroupedGroupFolder.cs @@ -1,9 +1,11 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; +using LightlessSync.UI; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.UI.Handlers; +using LightlessSync.UI.Style; using LightlessSync.UI.Models; using LightlessSync.WebAPI; using System.Collections.Immutable; @@ -22,6 +24,7 @@ public class DrawGroupedGroupFolder : IDrawFolder private readonly RenameSyncshellTagUi _renameSyncshellTagUi; private bool _wasHovered = false; private float _menuWidth; + private bool _rowClickArmed; public IImmutableList DrawPairs => _groups.SelectMany(g => g.GroupDrawFolder.DrawPairs).ToImmutableList(); public int OnlinePairs => _groups.SelectMany(g => g.GroupDrawFolder.DrawPairs).Where(g => g.Pair.IsOnline).DistinctBy(g => g.Pair.UserData.UID).Count(); @@ -48,7 +51,9 @@ public class DrawGroupedGroupFolder : IDrawFolder using var id = ImRaii.PushId(_id); var color = ImRaii.PushColor(ImGuiCol.ChildBg, ImGui.GetColorU32(ImGuiCol.FrameBgHovered), _wasHovered); - using (ImRaii.Child("folder__" + _id, new Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight()))) + var allowRowClick = string.IsNullOrEmpty(_tag); + var suppressRowToggle = false; + using (ImRaii.Child("folder__" + _id, new System.Numerics.Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight()))) { ImGui.Dummy(new Vector2(0f, ImGui.GetFrameHeight())); using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(0f, 0f))) @@ -61,6 +66,7 @@ public class DrawGroupedGroupFolder : IDrawFolder if (ImGui.IsItemClicked()) { _tagHandler.SetTagOpen(_id, !_tagHandler.IsTagOpen(_id)); + suppressRowToggle = true; } ImGui.SameLine(); @@ -92,7 +98,7 @@ public class DrawGroupedGroupFolder : IDrawFolder ImGui.SameLine(); DrawPauseButton(); ImGui.SameLine(); - DrawMenu(); + DrawMenu(ref suppressRowToggle); } else { ImGui.TextUnformatted("All Syncshells"); @@ -102,7 +108,36 @@ public class DrawGroupedGroupFolder : IDrawFolder } } color.Dispose(); - _wasHovered = ImGui.IsItemHovered(); + var rowHovered = ImGui.IsItemHovered(); + _wasHovered = rowHovered; + + if (allowRowClick) + { + if (rowHovered && ImGui.IsMouseClicked(ImGuiMouseButton.Left) && !suppressRowToggle) + { + _rowClickArmed = true; + } + + if (_rowClickArmed && rowHovered && ImGui.IsMouseReleased(ImGuiMouseButton.Left)) + { + _tagHandler.SetTagOpen(_id, !_tagHandler.IsTagOpen(_id)); + _rowClickArmed = false; + } + + if (!ImGui.IsMouseDown(ImGuiMouseButton.Left)) + { + _rowClickArmed = false; + } + } + else + { + _rowClickArmed = false; + } + + if (_wasHovered) + { + Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), spanFullWidth: true); + } ImGui.Separator(); @@ -154,7 +189,7 @@ public class DrawGroupedGroupFolder : IDrawFolder } } - protected void DrawMenu() + protected void DrawMenu(ref bool suppressRowToggle) { var barButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.EllipsisV); var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth(); @@ -162,6 +197,7 @@ public class DrawGroupedGroupFolder : IDrawFolder ImGui.SameLine(windowEndX - barButtonSize.X); if (_uiSharedService.IconButton(FontAwesomeIcon.EllipsisV)) { + suppressRowToggle = true; ImGui.OpenPopup("User Flyout Menu"); } if (ImGui.BeginPopup("User Flyout Menu")) diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index 4c4c1d4..96b9ea4 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -12,9 +12,10 @@ using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Handlers; +using LightlessSync.UI.Models; +using LightlessSync.UI.Style; using LightlessSync.Utils; using LightlessSync.WebAPI; -using System.Collections.Generic; using System.Collections.Immutable; using System.Text; @@ -27,29 +28,43 @@ public class DrawUserPair protected readonly LightlessMediator _mediator; protected readonly List _syncedGroups; private readonly GroupFullInfoDto? _currentGroup; - protected Pair _pair; + protected Pair? _pair; + private PairUiEntry _uiEntry; + protected PairDisplayEntry _displayEntry; private readonly string _id; private readonly SelectTagForPairUi _selectTagForPairUi; private readonly ServerConfigurationManager _serverConfigurationManager; private readonly UiSharedService _uiSharedService; private readonly PlayerPerformanceConfigService _performanceConfigService; + private readonly LightlessConfigService _configService; private readonly CharaDataManager _charaDataManager; + private readonly PairLedger _pairLedger; private float _menuWidth = -1; private bool _wasHovered = false; private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty; private string _cachedTooltip = string.Empty; - public DrawUserPair(string id, Pair entry, List syncedGroups, + public DrawUserPair( + string id, + PairUiEntry uiEntry, + Pair? legacyPair, GroupFullInfoDto? currentGroup, - ApiController apiController, IdDisplayHandler uIDDisplayHandler, - LightlessMediator lightlessMediator, SelectTagForPairUi selectTagForPairUi, + ApiController apiController, + IdDisplayHandler uIDDisplayHandler, + LightlessMediator lightlessMediator, + SelectTagForPairUi selectTagForPairUi, ServerConfigurationManager serverConfigurationManager, - UiSharedService uiSharedService, PlayerPerformanceConfigService performanceConfigService, - CharaDataManager charaDataManager) + UiSharedService uiSharedService, + PlayerPerformanceConfigService performanceConfigService, + LightlessConfigService configService, + CharaDataManager charaDataManager, + PairLedger pairLedger) { _id = id; - _pair = entry; - _syncedGroups = syncedGroups; + _uiEntry = uiEntry; + _displayEntry = uiEntry.DisplayEntry; + _pair = legacyPair ?? throw new ArgumentNullException(nameof(legacyPair)); + _syncedGroups = uiEntry.DisplayEntry.Groups.ToList(); _currentGroup = currentGroup; _apiController = apiController; _displayHandler = uIDDisplayHandler; @@ -58,7 +73,20 @@ public class DrawUserPair _serverConfigurationManager = serverConfigurationManager; _uiSharedService = uiSharedService; _performanceConfigService = performanceConfigService; + _configService = configService; _charaDataManager = charaDataManager; + _pairLedger = pairLedger; + } + + public PairDisplayEntry DisplayEntry => _displayEntry; + public PairUiEntry UiEntry => _uiEntry; + + public void UpdateDisplayEntry(PairUiEntry entry) + { + _uiEntry = entry; + _displayEntry = entry.DisplayEntry; + _syncedGroups.Clear(); + _syncedGroups.AddRange(entry.DisplayEntry.Groups); } public Pair Pair => _pair; @@ -77,6 +105,10 @@ public class DrawUserPair DrawName(posX, rightSide); } _wasHovered = ImGui.IsItemHovered(); + if (_wasHovered) + { + Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), spanFullWidth: true); + } color.Dispose(); } @@ -103,7 +135,7 @@ public class DrawUserPair if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true)) { - _ = _apiController.CyclePauseAsync(_pair.UserData); + _ = _apiController.CyclePauseAsync(_pair); ImGui.CloseCurrentPopup(); } ImGui.Separator(); @@ -197,6 +229,11 @@ public class DrawUserPair private void DrawLeftSide() { ImGui.AlignTextToFramePadding(); + + if (_pair == null) + { + return; + } if (_pair.IsPaused) { @@ -213,7 +250,19 @@ public class DrawUserPair } else if (_pair.IsVisible) { - _uiSharedService.IconText(FontAwesomeIcon.Eye, UIColors.Get("LightlessBlue")); + if (_configService.Current.ShowVisiblePairsGreenEye) + { + _uiSharedService.IconText(FontAwesomeIcon.Eye, UIColors.Get("LightlessGreen")); + } + else + { + _uiSharedService.IconText(FontAwesomeIcon.Eye, UIColors.Get("LightlessBlue")); + } + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem | ImGuiHoveredFlags.AllowWhenOverlapped | ImGuiHoveredFlags.AllowWhenDisabled)) + { + _mediator.Publish(new PairFocusCharacterMessage(_pair)); + } if (ImGui.IsItemClicked()) { _mediator.Publish(new TargetPairMessage(_pair)); @@ -313,6 +362,7 @@ public class DrawUserPair _pair.PlayerName ?? string.Empty, _pair.LastAppliedDataBytes, _pair.LastAppliedApproximateVRAMBytes, + _pair.LastAppliedApproximateEffectiveVRAMBytes, _pair.LastAppliedDataTris, _pair.IsPaired, groupDisplays is null ? ImmutableArray.Empty : ImmutableArray.CreateRange(groupDisplays)); @@ -381,7 +431,14 @@ public class DrawUserPair { builder.Append(Environment.NewLine); builder.Append("Approx. VRAM Usage: "); - builder.Append(UiSharedService.ByteToString(snapshot.LastAppliedApproximateVRAMBytes, true)); + var originalText = UiSharedService.ByteToString(snapshot.LastAppliedApproximateVRAMBytes, true); + builder.Append(originalText); + if (snapshot.LastAppliedApproximateEffectiveVRAMBytes >= 0) + { + builder.Append(" (Effective: "); + builder.Append(UiSharedService.ByteToString(snapshot.LastAppliedApproximateEffectiveVRAMBytes, true)); + builder.Append(')'); + } } if (snapshot.LastAppliedDataTris >= 0) @@ -420,12 +477,13 @@ public class DrawUserPair string PlayerName, long LastAppliedDataBytes, long LastAppliedApproximateVRAMBytes, + long LastAppliedApproximateEffectiveVRAMBytes, long LastAppliedDataTris, bool IsPaired, ImmutableArray GroupDisplays) { public static TooltipSnapshot Empty { get; } = - new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, false, ImmutableArray.Empty); + new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, -1, false, ImmutableArray.Empty); } private void DrawPairedClientMenu() @@ -647,7 +705,13 @@ public class DrawUserPair private void DrawSyncshellMenu(GroupFullInfoDto group, bool selfIsOwner, bool selfIsModerator, bool userIsPinned, bool userIsModerator) { - if (selfIsOwner || ((selfIsModerator) && (!userIsModerator))) + var showModeratorActions = selfIsOwner || (selfIsModerator && !userIsModerator); + var showOwnerActions = selfIsOwner; + + if (showModeratorActions || showOwnerActions) + ImGui.Separator(); + + if (showModeratorActions) { ImGui.TextUnformatted("Syncshell Moderator Functions"); var pinText = userIsPinned ? "Unpin user" : "Pin user"; @@ -683,7 +747,7 @@ public class DrawUserPair ImGui.Separator(); } - if (selfIsOwner) + if (showOwnerActions) { ImGui.TextUnformatted("Syncshell Owner Functions"); string modText = userIsModerator ? "Demod user" : "Mod user"; diff --git a/LightlessSync/UI/Components/Popup/BanUserPopupHandler.cs b/LightlessSync/UI/Components/Popup/BanUserPopupHandler.cs index b1cb3f8..ee80074 100644 --- a/LightlessSync/UI/Components/Popup/BanUserPopupHandler.cs +++ b/LightlessSync/UI/Components/Popup/BanUserPopupHandler.cs @@ -1,6 +1,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using LightlessSync.API.Dto.Group; +using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; using LightlessSync.WebAPI; @@ -12,14 +13,16 @@ public class BanUserPopupHandler : IPopupHandler { private readonly ApiController _apiController; private readonly UiSharedService _uiSharedService; + private readonly PairFactory _pairFactory; private string _banReason = string.Empty; private GroupFullInfoDto _group = null!; private Pair _reportedPair = null!; - public BanUserPopupHandler(ApiController apiController, UiSharedService uiSharedService) + public BanUserPopupHandler(ApiController apiController, UiSharedService uiSharedService, PairFactory pairFactory) { _apiController = apiController; _uiSharedService = uiSharedService; + _pairFactory = pairFactory; } public Vector2 PopupSize => new(500, 250); @@ -43,7 +46,7 @@ public class BanUserPopupHandler : IPopupHandler public void Open(OpenBanUserPopupMessage message) { - _reportedPair = message.PairToBan; + _reportedPair = _pairFactory.Create(message.PairToBan.UniqueIdent) ?? message.PairToBan; _group = message.GroupFullInfoDto; _banReason = string.Empty; } diff --git a/LightlessSync/UI/Components/SelectPairForTagUi.cs b/LightlessSync/UI/Components/SelectPairForTagUi.cs index 89db40e..a2ae7d1 100644 --- a/LightlessSync/UI/Components/SelectPairForTagUi.cs +++ b/LightlessSync/UI/Components/SelectPairForTagUi.cs @@ -23,7 +23,7 @@ public class SelectPairForTagUi _uidDisplayHandler = uidDisplayHandler; } - public void Draw(List pairs) + public void Draw(IReadOnlyList pairs) { var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale; var minSize = new Vector2(300, workHeight < 400 ? workHeight : 400) * ImGuiHelpers.GlobalScale; diff --git a/LightlessSync/UI/Components/SelectSyncshellForTagUi.cs b/LightlessSync/UI/Components/SelectSyncshellForTagUi.cs index 62dd1d6..63e48e0 100644 --- a/LightlessSync/UI/Components/SelectSyncshellForTagUi.cs +++ b/LightlessSync/UI/Components/SelectSyncshellForTagUi.cs @@ -21,7 +21,7 @@ public class SelectSyncshellForTagUi _tagHandler = tagHandler; } - public void Draw(List groups) + public void Draw(IReadOnlyCollection groups) { var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale; var minSize = new Vector2(300, workHeight < 400 ? workHeight : 400) * ImGuiHelpers.GlobalScale; diff --git a/LightlessSync/UI/CreateSyncshellUI.cs b/LightlessSync/UI/CreateSyncshellUI.cs index 215156b..2198a42 100644 --- a/LightlessSync/UI/CreateSyncshellUI.cs +++ b/LightlessSync/UI/CreateSyncshellUI.cs @@ -5,6 +5,7 @@ using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using System.Numerics; @@ -24,13 +25,10 @@ public class CreateSyncshellUI : WindowMediatorSubscriberBase { _apiController = apiController; _uiSharedService = uiSharedService; - SizeConstraints = new() - { - MinimumSize = new(550, 330), - MaximumSize = new(550, 330) - }; - - Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse; + WindowBuilder.For(this) + .SetFixedSize(new Vector2(550, 330)) + .AddFlags(ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse) + .Apply(); Mediator.Subscribe(this, (_) => IsOpen = false); } diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index 5b750f3..a4bbf9f 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -1,6 +1,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Data.Enum; @@ -9,41 +10,96 @@ using LightlessSync.Interop.Ipc; using LightlessSync.LightlessConfiguration; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Services.TextureCompression; using LightlessSync.Utils; using Microsoft.Extensions.Logging; +using OtterTex; +using System.Globalization; using System.Numerics; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using ImageSharpImage = SixLabors.ImageSharp.Image; namespace LightlessSync.UI; public class DataAnalysisUi : WindowMediatorSubscriberBase { + private const float MinTextureFilterPaneWidth = 305f; + private const float MaxTextureFilterPaneWidth = 405f; + private const float MinTextureDetailPaneWidth = 480f; + private const float MaxTextureDetailPaneWidth = 720f; + private const float TextureFilterSplitterWidth = 8f; + private const float TextureDetailSplitterWidth = 12f; + private const float TextureDetailSplitterCollapsedWidth = 18f; + private const float SelectedFilePanelLogicalHeight = 90f; + private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f); + private readonly CharacterAnalyzer _characterAnalyzer; - private readonly Progress<(string, int)> _conversionProgress = new(); + private readonly Progress _conversionProgress = new(); private readonly IpcManager _ipcManager; private readonly UiSharedService _uiSharedService; private readonly PlayerPerformanceConfigService _playerPerformanceConfig; private readonly TransientResourceManager _transientResourceManager; private readonly TransientConfigService _transientConfigService; - private readonly Dictionary _texturesToConvert = new(StringComparer.Ordinal); + private readonly TextureCompressionService _textureCompressionService; + private readonly TextureMetadataHelper _textureMetadataHelper; + + private readonly List _textureRows = new(); + private readonly Dictionary _textureSelections = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _selectedTextureKeys = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _texturePreviews = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _textureWorkspaceTabs = new(); + private readonly List _storedPathsToRemove = []; + private readonly Dictionary _filePathResolve = []; + private Dictionary>? _cachedAnalysis; private CancellationTokenSource _conversionCancellationTokenSource = new(); - private string _conversionCurrentFileName = string.Empty; - private int _conversionCurrentFileProgress = 0; + private CancellationTokenSource _transientRecordCts = new(); + private Task? _conversionTask; - private bool _enableBc7ConversionMode = false; - private bool _hasUpdate = false; - private bool _modalOpen = false; + private TextureConversionProgress? _lastConversionProgress; + + private float _textureFilterPaneWidth = 320f; + private float _textureDetailPaneWidth = 360f; + private float _textureDetailHeight = 360f; + private float _texturePreviewSize = 360f; + + private string _conversionCurrentFileName = string.Empty; private string _selectedFileTypeTab = string.Empty; private string _selectedHash = string.Empty; - private ObjectKind _selectedObjectTab; + private string _textureSearch = string.Empty; + private string _textureSlotFilter = "All"; + private string _selectedTextureKey = string.Empty; + private string _selectedStoredCharacter = string.Empty; + private string _selectedJobEntry = string.Empty; + private string _filterGamePath = string.Empty; + private string _filterFilePath = string.Empty; + + private int _conversionCurrentFileProgress = 0; + private int _conversionTotalJobs; + + private bool _hasUpdate = false; + private bool _modalOpen = false; private bool _showModal = false; - private CancellationTokenSource _transientRecordCts = new(); + private bool _textureRowsDirty = true; + private bool _textureDetailCollapsed = false; + private bool _conversionFailed; + private bool _showAlreadyAddedTransients = false; + private bool _acknowledgeReview = false; + + private ObjectKind _selectedObjectTab; + + private TextureUsageCategory? _textureCategoryFilter = null; + private TextureMapKind? _textureMapFilter = null; + private TextureCompressionTarget? _textureTargetFilter = null; public DataAnalysisUi(ILogger logger, LightlessMediator mediator, CharacterAnalyzer characterAnalyzer, IpcManager ipcManager, PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService, PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager, - TransientConfigService transientConfigService) + TransientConfigService transientConfigService, TextureCompressionService textureCompressionService, + TextureMetadataHelper textureMetadataHelper) : base(logger, mediator, "Lightless Character Data Analysis", performanceCollectorService) { _characterAnalyzer = characterAnalyzer; @@ -52,114 +108,154 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _playerPerformanceConfig = playerPerformanceConfig; _transientResourceManager = transientResourceManager; _transientConfigService = transientConfigService; + _textureCompressionService = textureCompressionService; + _textureMetadataHelper = textureMetadataHelper; Mediator.Subscribe(this, (_) => { _hasUpdate = true; }); - SizeConstraints = new() - { - MinimumSize = new() - { - X = 800, - Y = 600 - }, - MaximumSize = new() - { - X = 3840, - Y = 2160 - } - }; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(1240, 680), new Vector2(3840, 2160)) + .Apply(); _conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged; } protected override void DrawInternal() { - if (_conversionTask != null && !_conversionTask.IsCompleted) + HandleConversionModal(); + RefreshAnalysisCache(); + DrawContentTabs(); + } + + private void HandleConversionModal() + { + if (_conversionTask == null) { - _showModal = true; - if (ImGui.BeginPopupModal("BC7 Conversion in Progress")) - { - ImGui.TextUnformatted("BC7 Conversion in progress: " + _conversionCurrentFileProgress + "/" + _texturesToConvert.Count); - UiSharedService.TextWrapped("Current file: " + _conversionCurrentFileName); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion")) - { - _conversionCancellationTokenSource.Cancel(); - } - UiSharedService.SetScaledWindowSize(500); - ImGui.EndPopup(); - } - else - { - _modalOpen = false; - } + return; } - else if (_conversionTask != null && _conversionTask.IsCompleted && _texturesToConvert.Count > 0) + + if (_conversionTask.IsCompleted) + { + ResetConversionModalState(); + return; + } + + _showModal = true; + if (ImGui.BeginPopupModal("Texture Compression in Progress", ImGuiWindowFlags.AlwaysAutoResize)) + { + DrawConversionModalContent(); + ImGui.EndPopup(); + } + else { - _conversionTask = null; - _texturesToConvert.Clear(); - _showModal = false; _modalOpen = false; - _enableBc7ConversionMode = false; } if (_showModal && !_modalOpen) { - ImGui.OpenPopup("BC7 Conversion in Progress"); + ImGui.OpenPopup("Texture Compression in Progress"); _modalOpen = true; } - - if (_hasUpdate) - { - _cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone(); - _hasUpdate = false; - } - - using var tabBar = ImRaii.TabBar("analysisRecordingTabBar"); - using (var tabItem = ImRaii.TabItem("Analysis")) - { - if (tabItem) - { - using var id = ImRaii.PushId("analysis"); - DrawAnalysis(); - } - } - using (var tabItem = ImRaii.TabItem("Transient Files")) - { - if (tabItem) - { - using var tabbar = ImRaii.TabBar("transientData"); - - using (var transientData = ImRaii.TabItem("Stored Transient File Data")) - { - using var id = ImRaii.PushId("data"); - - if (transientData) - { - DrawStoredData(); - } - } - using (var transientRecord = ImRaii.TabItem("Record Transient Data")) - { - using var id = ImRaii.PushId("recording"); - - if (transientRecord) - { - DrawRecording(); - } - } - } - } } - private bool _showAlreadyAddedTransients = false; - private bool _acknowledgeReview = false; - private string _selectedStoredCharacter = string.Empty; - private string _selectedJobEntry = string.Empty; - private readonly List _storedPathsToRemove = []; - private readonly Dictionary _filePathResolve = []; - private string _filterGamePath = string.Empty; - private string _filterFilePath = string.Empty; + private void DrawConversionModalContent() + { + var progress = _lastConversionProgress; + var total = progress?.Total ?? Math.Max(_conversionTotalJobs, 1); + var completed = progress != null + ? Math.Min(progress.Completed + 1, total) + : _conversionCurrentFileProgress; + var currentLabel = !string.IsNullOrEmpty(_conversionCurrentFileName) + ? _conversionCurrentFileName + : "Preparing..."; + + ImGui.TextUnformatted($"Compressing textures ({completed}/{total})"); + UiSharedService.TextWrapped("Current file: " + currentLabel); + + if (_conversionFailed) + { + UiSharedService.ColorText("Conversion encountered errors. Please review the log for details.", ImGuiColors.DalamudRed); + } + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion")) + { + _conversionCancellationTokenSource.Cancel(); + } + + UiSharedService.SetScaledWindowSize(520); + } + + private void ResetConversionModalState() + { + _conversionTask = null; + _showModal = false; + _modalOpen = false; + _lastConversionProgress = null; + _conversionCurrentFileName = string.Empty; + _conversionCurrentFileProgress = 0; + _conversionTotalJobs = 0; + } + + private void RefreshAnalysisCache() + { + if (!_hasUpdate) + { + return; + } + + _cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone(); + _hasUpdate = false; + _textureRowsDirty = true; + } + + private void DrawContentTabs() + { + using var tabBar = ImRaii.TabBar("analysisRecordingTabBar"); + DrawAnalysisTab(); + DrawTransientFilesTab(); + } + + private void DrawAnalysisTab() + { + using var tabItem = ImRaii.TabItem("Analysis"); + if (!tabItem) + { + return; + } + + using var id = ImRaii.PushId("analysis"); + DrawAnalysis(); + } + + private void DrawTransientFilesTab() + { + using var tabItem = ImRaii.TabItem("Transient Files"); + if (!tabItem) + { + return; + } + + using var tabbar = ImRaii.TabBar("transientData"); + + using (var transientData = ImRaii.TabItem("Stored Transient File Data")) + { + using var id = ImRaii.PushId("data"); + if (transientData) + { + DrawStoredData(); + } + } + + using (var transientRecord = ImRaii.TabItem("Record Transient Data")) + { + using var id = ImRaii.PushId("recording"); + if (transientRecord) + { + DrawRecording(); + } + } + } private void DrawStoredData() { @@ -176,191 +272,258 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase var config = _transientConfigService.Current.TransientConfigs; Vector2 availableContentRegion = Vector2.Zero; - using (ImRaii.Group()) + DrawCharacterColumn(); + + ImGui.SameLine(); + + bool selectedData = config.TryGetValue(_selectedStoredCharacter, out var transientStorage) && transientStorage != null; + DrawJobColumn(); + + ImGui.SameLine(); + DrawAttachedFilesColumn(); + + return; + + void DrawCharacterColumn() { - ImGui.TextUnformatted("Character"); - ImGui.Separator(); - ImGuiHelpers.ScaledDummy(3); - availableContentRegion = ImGui.GetContentRegionAvail(); - using (ImRaii.ListBox("##characters", new Vector2(200, availableContentRegion.Y))) + using (ImRaii.Group()) { - foreach (var entry in config) + ImGui.TextUnformatted("Character"); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(3); + availableContentRegion = ImGui.GetContentRegionAvail(); + using (ImRaii.ListBox("##characters", new Vector2(200, availableContentRegion.Y))) { - var name = entry.Key.Split("_"); - if (!_uiSharedService.WorldData.TryGetValue(ushort.Parse(name[1]), out var worldname)) + foreach (var entry in config) { - continue; - } - if (ImGui.Selectable(name[0] + " (" + worldname + ")", string.Equals(_selectedStoredCharacter, entry.Key, StringComparison.Ordinal))) - { - _selectedStoredCharacter = entry.Key; - _selectedJobEntry = string.Empty; - _storedPathsToRemove.Clear(); - _filePathResolve.Clear(); - _filterFilePath = string.Empty; - _filterGamePath = string.Empty; + var name = entry.Key.Split("_"); + if (!_uiSharedService.WorldData.TryGetValue(ushort.Parse(name[1]), out var worldname)) + { + continue; + } + + bool isSelected = string.Equals(_selectedStoredCharacter, entry.Key, StringComparison.Ordinal); + if (ImGui.Selectable(name[0] + " (" + worldname + ")", isSelected)) + { + _selectedStoredCharacter = entry.Key; + _selectedJobEntry = string.Empty; + ResetSelectionFilters(); + } } } } } - ImGui.SameLine(); - bool selectedData = config.TryGetValue(_selectedStoredCharacter, out var transientStorage) && transientStorage != null; - using (ImRaii.Group()) + + void DrawJobColumn() { - ImGui.TextUnformatted("Job"); - ImGui.Separator(); - ImGuiHelpers.ScaledDummy(3); - using (ImRaii.ListBox("##data", new Vector2(150, availableContentRegion.Y))) + using (ImRaii.Group()) { - if (selectedData) + ImGui.TextUnformatted("Job"); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(3); + using (ImRaii.ListBox("##data", new Vector2(150, availableContentRegion.Y))) { + if (!selectedData) + { + return; + } + if (ImGui.Selectable("All Jobs", string.Equals(_selectedJobEntry, "alljobs", StringComparison.Ordinal))) { _selectedJobEntry = "alljobs"; } + foreach (var job in transientStorage!.JobSpecificCache) { - if (!_uiSharedService.JobData.TryGetValue(job.Key, out var jobName)) continue; + if (!_uiSharedService.JobData.TryGetValue(job.Key, out var jobName)) + { + continue; + } + if (ImGui.Selectable(jobName, string.Equals(_selectedJobEntry, job.Key.ToString(), StringComparison.Ordinal))) { _selectedJobEntry = job.Key.ToString(); + ResetSelectionFilters(); + } + } + } + } + } + + void DrawAttachedFilesColumn() + { + using (ImRaii.Group()) + { + var selectedList = string.Equals(_selectedJobEntry, "alljobs", StringComparison.Ordinal) + ? config[_selectedStoredCharacter].GlobalPersistentCache + : (string.IsNullOrEmpty(_selectedJobEntry) ? [] : config[_selectedStoredCharacter].JobSpecificCache[uint.Parse(_selectedJobEntry)]); + ImGui.TextUnformatted($"Attached Files (Total Files: {selectedList.Count})"); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(3); + using (ImRaii.Disabled(string.IsNullOrEmpty(_selectedJobEntry))) + { + var restContent = availableContentRegion.X - ImGui.GetCursorPosX(); + using var group = ImRaii.Group(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Resolve Game Paths to used File Paths")) + { + _ = Task.Run(async () => + { + if (!_ipcManager.Penumbra.APIAvailable) + { + return; + } + + var paths = selectedList.ToArray(); + var (forward, _) = await _ipcManager.Penumbra.ResolvePathsAsync(paths, Array.Empty()).ConfigureAwait(false); + for (int i = 0; i < paths.Length && i < forward.Length; i++) + { + var result = forward[i]; + if (string.IsNullOrEmpty(result)) + { + continue; + } + + if (!_filePathResolve.TryAdd(paths[i], result)) + { + _filePathResolve[paths[i]] = result; + } + } + }); + } + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Eraser, "Clear Game Path File Resolves")) + { + _filePathResolve.Clear(); + } + ImGui.SameLine(); + using (ImRaii.Disabled(!_storedPathsToRemove.Any())) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Selected Game Paths")) + { + foreach (var entry in _storedPathsToRemove) + { + config[_selectedStoredCharacter].GlobalPersistentCache.Remove(entry); + foreach (var job in config[_selectedStoredCharacter].JobSpecificCache) + { + job.Value.Remove(entry); + } + } + _storedPathsToRemove.Clear(); - _filePathResolve.Clear(); + _transientConfigService.Save(); + _transientResourceManager.RebuildSemiTransientResources(); _filterFilePath = string.Empty; _filterGamePath = string.Empty; } } + ImGui.SameLine(); + using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear ALL Game Paths")) + { + selectedList.Clear(); + _transientConfigService.Save(); + _transientResourceManager.RebuildSemiTransientResources(); + _filterFilePath = string.Empty; + _filterGamePath = string.Empty; + } + } + UiSharedService.AttachToolTip("Hold CTRL to delete all game paths from the displayed list" + + UiSharedService.TooltipSeparator + "You usually do not need to do this. All animation and VFX data will be automatically handled through Lightless."); + ImGuiHelpers.ScaledDummy(5); + ImGuiHelpers.ScaledDummy(30); + ImGui.SameLine(); + ImGui.SetNextItemWidth((restContent - 30) / 2f); + ImGui.InputTextWithHint("##filterGamePath", "Filter by Game Path", ref _filterGamePath, 255); + ImGui.SameLine(); + ImGui.SetNextItemWidth((restContent - 30) / 2f); + ImGui.InputTextWithHint("##filterFilePath", "Filter by File Path", ref _filterFilePath, 255); + + using (var dataTable = ImRaii.Table("##table", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg)) + { + if (dataTable) + { + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 30); + ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthFixed, (restContent - 30) / 2f); + ImGui.TableSetupColumn("File Path", ImGuiTableColumnFlags.WidthFixed, (restContent - 30) / 2f); + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + int id = 0; + foreach (var entry in selectedList) + { + if (!string.IsNullOrWhiteSpace(_filterGamePath) && !entry.Contains(_filterGamePath, StringComparison.OrdinalIgnoreCase)) + continue; + bool hasFileResolve = _filePathResolve.TryGetValue(entry, out var filePath); + + if (hasFileResolve && !string.IsNullOrEmpty(_filterFilePath) && !filePath!.Contains(_filterFilePath, StringComparison.OrdinalIgnoreCase)) + continue; + + using var imguiid = ImRaii.PushId(id++); + ImGui.TableNextColumn(); + bool isSelected = _storedPathsToRemove.Contains(entry, StringComparer.Ordinal); + if (ImGui.Checkbox("##", ref isSelected)) + { + if (isSelected) + _storedPathsToRemove.Add(entry); + else + _storedPathsToRemove.Remove(entry); + } + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry); + UiSharedService.AttachToolTip(entry + UiSharedService.TooltipSeparator + "Click to copy to clipboard"); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + ImGui.SetClipboardText(entry); + } + ImGui.TableNextColumn(); + if (hasFileResolve) + { + ImGui.TextUnformatted(filePath ?? "Unk"); + UiSharedService.AttachToolTip(filePath ?? "Unk" + UiSharedService.TooltipSeparator + "Click to copy to clipboard"); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + ImGui.SetClipboardText(filePath); + } + } + else + { + ImGui.TextUnformatted("-"); + UiSharedService.AttachToolTip("Resolve Game Paths to used File Paths to display the associated file paths."); + } + } + } + } } } } - ImGui.SameLine(); - using (ImRaii.Group()) + + void ResetSelectionFilters() { - var selectedList = string.Equals(_selectedJobEntry, "alljobs", StringComparison.Ordinal) - ? config[_selectedStoredCharacter].GlobalPersistentCache - : (string.IsNullOrEmpty(_selectedJobEntry) ? [] : config[_selectedStoredCharacter].JobSpecificCache[uint.Parse(_selectedJobEntry)]); - ImGui.TextUnformatted($"Attached Files (Total Files: {selectedList.Count})"); - ImGui.Separator(); - ImGuiHelpers.ScaledDummy(3); - using (ImRaii.Disabled(string.IsNullOrEmpty(_selectedJobEntry))) - { - - var restContent = availableContentRegion.X - ImGui.GetCursorPosX(); - using var group = ImRaii.Group(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Resolve Game Paths to used File Paths")) - { - _ = Task.Run(async () => - { - var paths = selectedList.ToArray(); - var resolved = await _ipcManager.Penumbra.ResolvePathsAsync(paths, []).ConfigureAwait(false); - _filePathResolve.Clear(); - - for (int i = 0; i < resolved.forward.Length; i++) - { - _filePathResolve[paths[i]] = resolved.forward[i]; - } - }); - } - ImGui.SameLine(); - ImGuiHelpers.ScaledDummy(20, 1); - ImGui.SameLine(); - using (ImRaii.Disabled(!_storedPathsToRemove.Any())) - { - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove selected Game Paths")) - { - foreach (var item in _storedPathsToRemove) - { - selectedList.Remove(item); - } - - _transientConfigService.Save(); - _transientResourceManager.RebuildSemiTransientResources(); - _filterFilePath = string.Empty; - _filterGamePath = string.Empty; - } - } - ImGui.SameLine(); - using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) - { - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear ALL Game Paths")) - { - selectedList.Clear(); - _transientConfigService.Save(); - _transientResourceManager.RebuildSemiTransientResources(); - _filterFilePath = string.Empty; - _filterGamePath = string.Empty; - } - } - UiSharedService.AttachToolTip("Hold CTRL to delete all game paths from the displayed list" - + UiSharedService.TooltipSeparator + "You usually do not need to do this. All animation and VFX data will be automatically handled through Lightless."); - ImGuiHelpers.ScaledDummy(5); - ImGuiHelpers.ScaledDummy(30); - ImGui.SameLine(); - ImGui.SetNextItemWidth((restContent - 30) / 2f); - ImGui.InputTextWithHint("##filterGamePath", "Filter by Game Path", ref _filterGamePath, 255); - ImGui.SameLine(); - ImGui.SetNextItemWidth((restContent - 30) / 2f); - ImGui.InputTextWithHint("##filterFilePath", "Filter by File Path", ref _filterFilePath, 255); - - using (var dataTable = ImRaii.Table("##table", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg)) - { - if (dataTable) - { - ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 30); - ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthFixed, (restContent - 30) / 2f); - ImGui.TableSetupColumn("File Path", ImGuiTableColumnFlags.WidthFixed, (restContent - 30) / 2f); - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableHeadersRow(); - int id = 0; - foreach (var entry in selectedList) - { - if (!string.IsNullOrWhiteSpace(_filterGamePath) && !entry.Contains(_filterGamePath, StringComparison.OrdinalIgnoreCase)) - continue; - bool hasFileResolve = _filePathResolve.TryGetValue(entry, out var filePath); - - if (hasFileResolve && !string.IsNullOrEmpty(_filterFilePath) && !filePath!.Contains(_filterFilePath, StringComparison.OrdinalIgnoreCase)) - continue; - - using var imguiid = ImRaii.PushId(id++); - ImGui.TableNextColumn(); - bool isSelected = _storedPathsToRemove.Contains(entry, StringComparer.Ordinal); - if (ImGui.Checkbox("##", ref isSelected)) - { - if (isSelected) - _storedPathsToRemove.Add(entry); - else - _storedPathsToRemove.Remove(entry); - } - ImGui.TableNextColumn(); - ImGui.TextUnformatted(entry); - UiSharedService.AttachToolTip(entry + UiSharedService.TooltipSeparator + "Click to copy to clipboard"); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) - { - ImGui.SetClipboardText(entry); - } - ImGui.TableNextColumn(); - if (hasFileResolve) - { - ImGui.TextUnformatted(filePath ?? "Unk"); - UiSharedService.AttachToolTip(filePath ?? "Unk" + UiSharedService.TooltipSeparator + "Click to copy to clipboard"); - if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) - { - ImGui.SetClipboardText(filePath); - } - } - else - { - ImGui.TextUnformatted("-"); - UiSharedService.AttachToolTip("Resolve Game Paths to used File Paths to display the associated file paths."); - } - } - } - } - } + _storedPathsToRemove.Clear(); + _filePathResolve.Clear(); + _filterFilePath = string.Empty; + _filterGamePath = string.Empty; } } + private void DrawRecording() + { + DrawRecordingHelpSection(); + DrawRecordingControlButtons(); + + if (_transientResourceManager.IsTransientRecording) + { + DrawRecordingActiveWarning(); + } + + ImGuiHelpers.ScaledDummy(5); + DrawRecordingReviewControls(); + ImGuiHelpers.ScaledDummy(5); + DrawRecordedTransientsTable(); + } + + private static void DrawRecordingHelpSection() { UiSharedService.DrawTree("What is this? (Explanation / Help)", () => { @@ -377,6 +540,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGuiColors.DalamudRed, 800); ImGuiHelpers.ScaledDummy(5); }); + } + + private void DrawRecordingControlButtons() + { using (ImRaii.Disabled(_transientResourceManager.IsTransientRecording)) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.Play, "Start Transient Recording")) @@ -396,15 +563,18 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _transientRecordCts.Cancel(); } } - if (_transientResourceManager.IsTransientRecording) - { - ImGui.SameLine(); - UiSharedService.ColorText($"RECORDING - Time Remaining: {_transientResourceManager.RecordTimeRemaining.Value}", UIColors.Get("LightlessYellow")); - ImGuiHelpers.ScaledDummy(5); - UiSharedService.DrawGroupedCenteredColorText("DO NOT CHANGE YOUR APPEARANCE OR MODS WHILE RECORDING, YOU CAN ACCIDENTALLY MAKE SOME OF YOUR APPEARANCE RELATED MODS PERMANENT.", ImGuiColors.DalamudRed, 800); - } + } + private void DrawRecordingActiveWarning() + { + ImGui.SameLine(); + UiSharedService.ColorText($"RECORDING - Time Remaining: {_transientResourceManager.RecordTimeRemaining.Value}", UIColors.Get("LightlessYellow")); ImGuiHelpers.ScaledDummy(5); + UiSharedService.DrawGroupedCenteredColorText("DO NOT CHANGE YOUR APPEARANCE OR MODS WHILE RECORDING, YOU CAN ACCIDENTALLY MAKE SOME OF YOUR APPEARANCE RELATED MODS PERMANENT.", ImGuiColors.DalamudRed, 800); + } + + private void DrawRecordingReviewControls() + { ImGui.Checkbox("Show previously added transient files in the recording", ref _showAlreadyAddedTransients); _uiSharedService.DrawHelpText("Use this only if you want to see what was previously already caught by Lightless"); ImGuiHelpers.ScaledDummy(5); @@ -425,49 +595,53 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase UiSharedService.DrawGroupedCenteredColorText("Please review the recorded mod files before saving and deselect files that got into the recording on accident.", UIColors.Get("LightlessYellow")); ImGuiHelpers.ScaledDummy(5); } + } - ImGuiHelpers.ScaledDummy(5); + private void DrawRecordedTransientsTable() + { var width = ImGui.GetContentRegionAvail(); using var table = ImRaii.Table("Recorded Transients", 4, ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); - if (table) + if (!table) { - int id = 0; - ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 30); - ImGui.TableSetupColumn("Owner", ImGuiTableColumnFlags.WidthFixed, 100); - ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthFixed, (width.X - 30 - 100) / 2f); - ImGui.TableSetupColumn("File Path", ImGuiTableColumnFlags.WidthFixed, (width.X - 30 - 100) / 2f); - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableHeadersRow(); - var transients = _transientResourceManager.RecordedTransients.ToList(); - transients.Reverse(); - foreach (var value in transients) - { - if (value.AlreadyTransient && !_showAlreadyAddedTransients) - continue; + return; + } - using var imguiid = ImRaii.PushId(id++); - if (value.AlreadyTransient) - { - ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); - } - ImGui.TableNextColumn(); - bool addTransient = value.AddTransient; - if (ImGui.Checkbox("##add", ref addTransient)) - { - value.AddTransient = addTransient; - } - ImGui.TableNextColumn(); - ImGui.TextUnformatted(value.Owner.Name); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(value.GamePath); - UiSharedService.AttachToolTip(value.GamePath); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(value.FilePath); - UiSharedService.AttachToolTip(value.FilePath); - if (value.AlreadyTransient) - { - ImGui.PopStyleColor(); - } + int id = 0; + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 30); + ImGui.TableSetupColumn("Owner", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthFixed, (width.X - 30 - 100) / 2f); + ImGui.TableSetupColumn("File Path", ImGuiTableColumnFlags.WidthFixed, (width.X - 30 - 100) / 2f); + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + var transients = _transientResourceManager.RecordedTransients.ToList(); + transients.Reverse(); + foreach (var value in transients) + { + if (value.AlreadyTransient && !_showAlreadyAddedTransients) + continue; + + using var imguiid = ImRaii.PushId(id++); + if (value.AlreadyTransient) + { + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); + } + ImGui.TableNextColumn(); + bool addTransient = value.AddTransient; + if (ImGui.Checkbox("##add", ref addTransient)) + { + value.AddTransient = addTransient; + } + ImGui.TableNextColumn(); + ImGui.TextUnformatted(value.Owner.Name); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(value.GamePath); + UiSharedService.AttachToolTip(value.GamePath); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(value.FilePath); + UiSharedService.AttachToolTip(value.FilePath); + if (value.AlreadyTransient) + { + ImGui.PopStyleColor(); } } } @@ -481,6 +655,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase if (_cachedAnalysis!.Count == 0) return; + EnsureTextureRows(); + bool isAnalyzing = _characterAnalyzer.IsAnalysisRunning; if (isAnalyzing) { @@ -513,31 +689,19 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGui.Separator(); - ImGui.TextUnformatted("Total files:"); - ImGui.SameLine(); - ImGui.TextUnformatted(_cachedAnalysis!.Values.Sum(c => c.Values.Count).ToString()); - ImGui.SameLine(); - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) - { - ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString()); - } - if (ImGui.IsItemHovered()) - { - string text = ""; - var groupedfiles = _cachedAnalysis.Values.SelectMany(f => f.Values).GroupBy(f => f.FileType, StringComparer.Ordinal); - text = string.Join(Environment.NewLine, groupedfiles.OrderBy(f => f.Key, StringComparer.Ordinal) - .Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize)) - + ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize)))); - ImGui.SetTooltip(text); - } - ImGui.TextUnformatted("Total size (actual):"); - ImGui.SameLine(); - ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize)))); - ImGui.TextUnformatted("Total size (compressed for up/download only):"); - ImGui.SameLine(); - ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize)))); - ImGui.TextUnformatted($"Total modded model triangles: {_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles))}"); - ImGui.Separator(); + var totalFileCount = _cachedAnalysis!.Values.Sum(c => c.Values.Count); + var totalActualSize = _cachedAnalysis.Sum(c => c.Value.Sum(entry => entry.Value.OriginalSize)); + var totalCompressedSize = _cachedAnalysis.Sum(c => c.Value.Sum(entry => entry.Value.CompressedSize)); + var totalTriangles = _cachedAnalysis.Sum(c => c.Value.Sum(entry => entry.Value.Triangles)); + var breakdown = string.Join(Environment.NewLine, + _cachedAnalysis.Values + .SelectMany(f => f.Values) + .GroupBy(f => f.FileType, StringComparer.Ordinal) + .OrderBy(f => f.Key, StringComparer.Ordinal) + .Select(f => $"{f.Key}: {f.Count()} files, size: {UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))}, compressed: {UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))}")); + + DrawAnalysisOverview(totalFileCount, totalActualSize, totalCompressedSize, totalTriangles, breakdown); + using var tabbar = ImRaii.TabBar("objectSelection"); foreach (var kvp in _cachedAnalysis) { @@ -549,221 +713,31 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal).OrderBy(k => k.Key, StringComparer.Ordinal).ToList(); - ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(1f, 1f)); - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1f, 1f)); - - if (ImGui.BeginTable($"##fileStats_{kvp.Key}", 3, - ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingFixedFit)) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"Files for {kvp.Key}"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(kvp.Value.Count.ToString()); - ImGui.SameLine(); - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) - ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString()); - if (ImGui.IsItemHovered()) - { - string text = string.Join(Environment.NewLine, groupedfiles.Select(f => - $"{f.Key}: {f.Count()} files, size: {UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))}, compressed: {UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))}")); - ImGui.SetTooltip(text); - } - ImGui.TableNextColumn(); - - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{kvp.Key} size (actual):"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize))); - ImGui.TableNextColumn(); - - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):"); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize))); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - ImGui.TableNextColumn(); - - var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal)); - if (vramUsage != null) - { - var actualVramUsage = vramUsage.Sum(f => f.OriginalSize); - - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{kvp.Key} VRAM usage:"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(UiSharedService.ByteToString(actualVramUsage)); - ImGui.TableNextColumn(); - - if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds - || _playerPerformanceConfig.Current.ShowPerformanceIndicator) - { - var currentVramWarning = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB; - - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.TextUnformatted("Configured VRAM threshold:"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{currentVramWarning} MiB."); - ImGui.TableNextColumn(); - if (currentVramWarning * 1024 * 1024 < actualVramUsage) - { - UiSharedService.ColorText( - $"You exceed your own threshold by {UiSharedService.ByteToString(actualVramUsage - (currentVramWarning * 1024 * 1024))}", - UIColors.Get("LightlessYellow")); - } - } - } - - var actualTriCount = kvp.Value.Sum(f => f.Value.Triangles); - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{kvp.Key} modded model triangles:"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(actualTriCount.ToString()); - ImGui.TableNextColumn(); - - if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds - || _playerPerformanceConfig.Current.ShowPerformanceIndicator) - { - var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands; - - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.TextUnformatted("Configured triangle threshold:"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{currentTriWarning * 1000} triangles."); - ImGui.TableNextColumn(); - if (currentTriWarning * 1000 < actualTriCount) - { - UiSharedService.ColorText( - $"You exceed your own threshold by {actualTriCount - (currentTriWarning * 1000)}", - UIColors.Get("LightlessYellow")); - } - } - - ImGui.EndTable(); - } - - ImGui.PopStyleVar(2); - - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); - - _uiSharedService.MediumText("Selected file:", UIColors.Get("LightlessBlue")); - ImGui.SameLine(); - _uiSharedService.MediumText(_selectedHash, UIColors.Get("LightlessYellow")); - - if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item)) - { - var filePaths = item.FilePaths; - UiSharedService.ColorText("Local file path:", UIColors.Get("LightlessBlue")); - ImGui.SameLine(); - UiSharedService.TextWrapped(filePaths[0]); - if (filePaths.Count > 1) - { - ImGui.SameLine(); - ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)"); - ImGui.SameLine(); - _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); - UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1))); - } - - var gamepaths = item.GamePaths; - UiSharedService.ColorText("Used by game path:", UIColors.Get("LightlessBlue")); - ImGui.SameLine(); - UiSharedService.TextWrapped(gamepaths[0]); - if (gamepaths.Count > 1) - { - ImGui.SameLine(); - ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)"); - ImGui.SameLine(); - _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); - UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1))); - } - } - - ImGui.Separator(); + DrawObjectOverview(kvp.Key, kvp.Value, groupedfiles); if (_selectedObjectTab != kvp.Key) { _selectedHash = string.Empty; _selectedObjectTab = kvp.Key; _selectedFileTypeTab = string.Empty; - _enableBc7ConversionMode = false; - _texturesToConvert.Clear(); } - using var fileTabBar = ImRaii.TabBar("fileTabs"); + var otherFileGroups = groupedfiles + .Where(g => !string.Equals(g.Key, "tex", StringComparison.Ordinal)) + .ToList(); - foreach (IGrouping? fileGroup in groupedfiles) + if (!string.IsNullOrEmpty(_selectedFileTypeTab) && + otherFileGroups.TrueForAll(g => !string.Equals(g.Key, _selectedFileTypeTab, StringComparison.Ordinal))) { - string fileGroupText = fileGroup.Key + " [" + fileGroup.Count() + "]"; - var requiresCompute = fileGroup.Any(k => !k.IsComputed); - using var tabcol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(UIColors.Get("LightlessYellow")), requiresCompute); - if (requiresCompute) - { - fileGroupText += " (!)"; - } - ImRaii.IEndObject fileTab; - using (var textcol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(new(0, 0, 0, 1)), - requiresCompute && !string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal))) - { - fileTab = ImRaii.TabItem(fileGroupText + "###" + fileGroup.Key); - } - - if (!fileTab) { fileTab.Dispose(); continue; } - - if (!string.Equals(fileGroup.Key, _selectedFileTypeTab, StringComparison.Ordinal)) - { - _selectedFileTypeTab = fileGroup.Key; - _selectedHash = string.Empty; - _enableBc7ConversionMode = false; - _texturesToConvert.Clear(); - } - - ImGui.TextUnformatted($"{fileGroup.Key} files"); - ImGui.SameLine(); - ImGui.TextUnformatted(fileGroup.Count().ToString()); - - ImGui.TextUnformatted($"{fileGroup.Key} files size (actual):"); - ImGui.SameLine(); - ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.OriginalSize))); - - ImGui.TextUnformatted($"{fileGroup.Key} files size (compressed for up/download only):"); - ImGui.SameLine(); - ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.CompressedSize))); - - if (string.Equals(_selectedFileTypeTab, "tex", StringComparison.Ordinal)) - { - ImGui.Checkbox("Enable BC7 Conversion Mode", ref _enableBc7ConversionMode); - if (_enableBc7ConversionMode) - { - UiSharedService.ColorText("WARNING BC7 CONVERSION:", UIColors.Get("LightlessYellow")); - ImGui.SameLine(); - UiSharedService.ColorText("Converting textures to BC7 is irreversible!", ImGuiColors.DalamudRed); - UiSharedService.ColorTextWrapped("- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures." + - Environment.NewLine + "- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts." + - Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." + - Environment.NewLine + "- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically." + - Environment.NewLine + "- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete." - , UIColors.Get("LightlessYellow")); - if (_texturesToConvert.Count > 0 && _uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start conversion of " + _texturesToConvert.Count + " texture(s)")) - { - _conversionCancellationTokenSource = _conversionCancellationTokenSource.CancelRecreate(); - _conversionTask = _ipcManager.Penumbra.ConvertTextureFiles(_logger, _texturesToConvert, _conversionProgress, _conversionCancellationTokenSource.Token); - } - } - } - - ImGui.Separator(); - DrawTable(fileGroup); - - fileTab.Dispose(); + _selectedFileTypeTab = string.Empty; } + + if (string.IsNullOrEmpty(_selectedFileTypeTab) && otherFileGroups.Count > 0) + { + _selectedFileTypeTab = otherFileGroups[0].Key; + } + + DrawTextureWorkspace(kvp.Key, otherFileGroups); } } } @@ -772,147 +746,2035 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { _hasUpdate = true; _selectedHash = string.Empty; - _enableBc7ConversionMode = false; - _texturesToConvert.Clear(); + _selectedTextureKey = string.Empty; + _selectedTextureKeys.Clear(); + _textureSelections.Clear(); + ResetTextureFilters(); + _textureRowsDirty = true; + _conversionFailed = false; } protected override void Dispose(bool disposing) { base.Dispose(disposing); + foreach (var preview in _texturePreviews.Values) + { + preview.Texture?.Dispose(); + } + _texturePreviews.Clear(); _conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged; } - private void ConversionProgress_ProgressChanged(object? sender, (string, int) e) + private void ConversionProgress_ProgressChanged(object? sender, TextureConversionProgress e) { - _conversionCurrentFileName = e.Item1; - _conversionCurrentFileProgress = e.Item2; + _lastConversionProgress = e; + _conversionTotalJobs = e.Total; + _conversionCurrentFileName = Path.GetFileName(e.CurrentJob.OutputFile); + _conversionCurrentFileProgress = Math.Min(e.Completed + 1, e.Total); + } + + private void EnsureTextureRows() + { + if (!_textureRowsDirty || _cachedAnalysis == null) + { + return; + } + + _textureRows.Clear(); + HashSet validKeys = new(StringComparer.OrdinalIgnoreCase); + + foreach (var (objectKind, entries) in _cachedAnalysis) + { + foreach (var entry in entries.Values) + { + if (!string.Equals(entry.FileType, "tex", StringComparison.Ordinal)) + { + continue; + } + + if (entry.FilePaths.Count == 0) + { + continue; + } + + var primaryFile = entry.FilePaths[0]; + var duplicatePaths = entry.FilePaths.Skip(1).ToList(); + var primaryGamePath = entry.GamePaths.FirstOrDefault() ?? string.Empty; + var classificationPath = string.IsNullOrEmpty(primaryGamePath) ? primaryFile : primaryGamePath; + var mapKind = _textureMetadataHelper.DetermineMapKind(primaryGamePath, primaryFile); + var category = TextureMetadataHelper.DetermineCategory(classificationPath); + var slot = TextureMetadataHelper.DetermineSlot(category, classificationPath); + var format = entry.Format.Value; + var suggestion = TextureMetadataHelper.GetSuggestedTarget(format, mapKind, classificationPath); + TextureCompressionTarget? currentTarget = TextureMetadataHelper.TryMapFormatToTarget(format, out var mappedTarget) + ? mappedTarget + : null; + var displayName = Path.GetFileName(primaryFile); + + var row = new TextureRow( + objectKind, + entry, + primaryFile, + duplicatePaths, + entry.GamePaths.ToList(), + primaryGamePath, + format, + mapKind, + category, + slot, + displayName, + currentTarget, + suggestion?.Target, + suggestion?.Reason); + + validKeys.Add(row.Key); + _textureRows.Add(row); + + if (row.IsAlreadyCompressed) + { + _selectedTextureKeys.Remove(row.Key); + _textureSelections.Remove(row.Key); + } + } + } + + _textureRows.Sort((a, b) => + { + var comp = a.ObjectKind.CompareTo(b.ObjectKind); + if (comp != 0) + return comp; + + comp = string.Compare(a.Slot, b.Slot, StringComparison.OrdinalIgnoreCase); + if (comp != 0) + return comp; + + return string.Compare(a.DisplayName, b.DisplayName, StringComparison.OrdinalIgnoreCase); + }); + + _selectedTextureKeys.RemoveWhere(key => !validKeys.Contains(key)); + + foreach (var key in _texturePreviews.Keys.ToArray()) + { + if (!validKeys.Contains(key) && _texturePreviews.TryGetValue(key, out var preview)) + { + preview.Texture?.Dispose(); + _texturePreviews.Remove(key); + } + } + + foreach (var key in _textureSelections.Keys.ToArray()) + { + if (!validKeys.Contains(key)) + { + _textureSelections.Remove(key); + continue; + } + + _textureSelections[key] = _textureCompressionService.NormalizeTarget(_textureSelections[key]); + } + + if (!string.IsNullOrEmpty(_selectedTextureKey) && !validKeys.Contains(_selectedTextureKey)) + { + _selectedTextureKey = string.Empty; + } + + _textureRowsDirty = false; + } + + private static string MakeTextureKey(ObjectKind objectKind, string primaryFilePath) => + $"{objectKind}|{primaryFilePath}".ToLowerInvariant(); + + private void ResetTextureFilters() + { + _textureCategoryFilter = null; + _textureSlotFilter = "All"; + _textureMapFilter = null; + _textureTargetFilter = null; + _textureSearch = string.Empty; + } + + private void DrawAnalysisOverview(int totalFiles, long totalActualSize, long totalCompressedSize, long totalTriangles, string breakdownTooltip) + { + var scale = ImGuiHelpers.GlobalScale; + var accent = UIColors.Get("LightlessGreen"); + var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.18f); + var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.4f); + var infoColor = ImGuiColors.DalamudGrey; + var diff = totalActualSize - totalCompressedSize; + string? diffText = null; + Vector4? diffColor = null; + if (diff > 0) + { + diffText = $"Saved {UiSharedService.ByteToString(diff)}"; + diffColor = UIColors.Get("LightlessGreen"); + } + else if (diff < 0) + { + diffText = $"Over by {UiSharedService.ByteToString(Math.Abs(diff))}"; + diffColor = UIColors.Get("DimRed"); + } + + var summaryHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 2.4f, 44f * scale); + + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize))) + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale))) + using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg))) + using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder))) + using (var child = ImRaii.Child("analysisOverview", new Vector2(-1f, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) + { + if (child) + { + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(8f * scale, 4f * scale))) + { + if (ImGui.BeginTable("analysisOverviewTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.NoHostExtendX)) + { + ImGui.TableNextRow(); + DrawSummaryCell(FontAwesomeIcon.ListUl, accent, $"{totalFiles:N0}", totalFiles == 1 ? "Tracked file" : "Tracked files", infoColor, scale, tooltip: breakdownTooltip); + DrawSummaryCell(FontAwesomeIcon.FileArchive, ImGuiColors.DalamudGrey, UiSharedService.ByteToString(totalActualSize), "Actual size", infoColor, scale); + DrawSummaryCell(FontAwesomeIcon.CompressArrowsAlt, UIColors.Get("LightlessYellow2"), UiSharedService.ByteToString(totalCompressedSize), "Compressed size", infoColor, scale, diffText, diffColor); + DrawSummaryCell(FontAwesomeIcon.ChartLine, UIColors.Get("LightlessPurple"), totalTriangles.ToString("N0", CultureInfo.InvariantCulture), "Modded triangles", infoColor, scale); + ImGui.EndTable(); + } + } + } + } + + ImGuiHelpers.ScaledDummy(6); + } + + private void DrawObjectOverview( + ObjectKind objectKind, + IReadOnlyDictionary entries, + IReadOnlyList> groupedFiles) + { + var scale = ImGuiHelpers.GlobalScale; + var accent = UIColors.Get("LightlessPurple"); + var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.16f); + var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.32f); + var infoColor = ImGuiColors.DalamudGrey; + var fileCount = entries.Count; + var actualSize = entries.Sum(c => c.Value.OriginalSize); + var compressedSize = entries.Sum(c => c.Value.CompressedSize); + var triangles = entries.Sum(c => c.Value.Triangles); + var breakdown = string.Join(Environment.NewLine, + groupedFiles.Select(f => + $"{f.Key}: {f.Count()} files, size: {UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))}, compressed: {UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))}")); + + var savings = actualSize - compressedSize; + string? compressedExtra = null; + Vector4? compressedExtraColor = null; + if (savings > 0) + { + compressedExtra = $"Saved {UiSharedService.ByteToString(savings)}"; + compressedExtraColor = UIColors.Get("LightlessGreen"); + } + else if (savings < 0) + { + compressedExtra = $"Over by {UiSharedService.ByteToString(Math.Abs(savings))}"; + compressedExtraColor = UIColors.Get("DimRed"); + } + + long actualVram = 0; + var vramGroup = groupedFiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal)); + if (vramGroup != null) + { + actualVram = vramGroup.Sum(f => f.OriginalSize); + } + + string? vramExtra = null; + Vector4? vramExtraColor = null; + var vramSub = vramGroup != null ? "VRAM usage" : "VRAM usage (no textures)"; + var showThresholds = _playerPerformanceConfig.Current.WarnOnExceedingThresholds + || _playerPerformanceConfig.Current.ShowPerformanceIndicator; + if (showThresholds && actualVram > 0) + { + var thresholdBytes = Math.Max(0, _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB) * 1024L * 1024L; + if (thresholdBytes > 0) + { + if (actualVram > thresholdBytes) + { + vramExtra = $"Over by {UiSharedService.ByteToString(actualVram - thresholdBytes)}"; + vramExtraColor = UIColors.Get("LightlessYellow"); + } + else + { + vramExtra = $"Remaining {UiSharedService.ByteToString(thresholdBytes - actualVram)}"; + vramExtraColor = UIColors.Get("LightlessGreen"); + } + } + } + + string? triExtra = null; + Vector4? triExtraColor = null; + if (showThresholds) + { + var triThreshold = Math.Max(0, _playerPerformanceConfig.Current.TrisWarningThresholdThousands) * 1000; + if (triThreshold > 0) + { + if (triangles > triThreshold) + { + triExtra = $"Over by {(triangles - triThreshold).ToString("N0", CultureInfo.InvariantCulture)}"; + triExtraColor = UIColors.Get("LightlessYellow"); + } + else + { + triExtra = $"Remaining {(triThreshold - triangles).ToString("N0", CultureInfo.InvariantCulture)}"; + triExtraColor = UIColors.Get("LightlessGreen"); + } + } + } + + var summaryHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 2.4f, 46f * scale); + var availableWidth = ImGui.GetContentRegionAvail().X; + var summaryWidth = objectKind == ObjectKind.Player + ? availableWidth + : MathF.Min(availableWidth, 760f * scale); + + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize))) + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale))) + using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg))) + using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder))) + using (var child = ImRaii.Child($"objectOverview##{objectKind}", new Vector2(summaryWidth, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) + { + if (child) + { + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(8f * scale, 4f * scale))) + { + if (ImGui.BeginTable($"objectOverviewTable##{objectKind}", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.NoHostExtendX)) + { + ImGui.TableNextRow(); + DrawSummaryCell(FontAwesomeIcon.ClipboardList, accent, $"{fileCount:N0}", $"{objectKind} files", infoColor, scale, tooltip: breakdown); + DrawSummaryCell(FontAwesomeIcon.FileArchive, ImGuiColors.DalamudGrey, UiSharedService.ByteToString(actualSize), "Actual size", infoColor, scale); + DrawSummaryCell(FontAwesomeIcon.CompressArrowsAlt, UIColors.Get("LightlessYellow2"), UiSharedService.ByteToString(compressedSize), "Compressed size", infoColor, scale, compressedExtra, compressedExtraColor); + DrawSummaryCell(FontAwesomeIcon.Memory, UIColors.Get("LightlessBlue"), UiSharedService.ByteToString(actualVram), vramSub, infoColor, scale, vramExtra, vramExtraColor); + DrawSummaryCell(FontAwesomeIcon.ProjectDiagram, UIColors.Get("LightlessPurple"), triangles.ToString("N0", CultureInfo.InvariantCulture), "Modded triangles", infoColor, scale, triExtra, triExtraColor); + ImGui.EndTable(); + } + } + } + } + + ImGuiHelpers.ScaledDummy(4); + } + + private enum TextureWorkspaceTab + { + Textures, + OtherFiles + } + + private sealed record TextureRow( + ObjectKind ObjectKind, + CharacterAnalyzer.FileDataEntry Entry, + string PrimaryFilePath, + IReadOnlyList DuplicateFilePaths, + IReadOnlyList GamePaths, + string PrimaryGamePath, + string Format, + TextureMapKind MapKind, + TextureUsageCategory Category, + string Slot, + string DisplayName, + TextureCompressionTarget? CurrentTarget, + TextureCompressionTarget? SuggestedTarget, + string? SuggestionReason) + { + public string Key { get; } = MakeTextureKey(ObjectKind, PrimaryFilePath); + public string Hash => Entry.Hash; + public long OriginalSize => Entry.OriginalSize; + public long CompressedSize => Entry.CompressedSize; + public bool IsComputed => Entry.IsComputed; + public bool IsAlreadyCompressed => CurrentTarget.HasValue; + } + + private sealed class TexturePreviewState + { + public Task? LoadTask { get; set; } + public IDalamudTextureWrap? Texture { get; set; } + public string? ErrorMessage { get; set; } + public DateTime LastAccessUtc { get; set; } = DateTime.UtcNow; + } + + private void DrawTextureWorkspace(ObjectKind objectKind, IReadOnlyList> otherFileGroups) + { + if (!_textureWorkspaceTabs.ContainsKey(objectKind)) + { + _textureWorkspaceTabs[objectKind] = TextureWorkspaceTab.Textures; + } + + if (otherFileGroups.Count == 0) + { + _textureWorkspaceTabs[objectKind] = TextureWorkspaceTab.Textures; + DrawTextureTabContent(objectKind); + return; + } + + using var tabBar = ImRaii.TabBar($"textureWorkspaceTabs##{objectKind}"); + + using (var texturesTab = ImRaii.TabItem($"Textures###textures_{objectKind}")) + { + if (texturesTab) + { + if (_textureWorkspaceTabs[objectKind] != TextureWorkspaceTab.Textures) + { + _textureWorkspaceTabs[objectKind] = TextureWorkspaceTab.Textures; + } + DrawTextureTabContent(objectKind); + } + } + + using (var otherFilesTab = ImRaii.TabItem($"Other file types###other_{objectKind}")) + { + if (otherFilesTab) + { + if (_textureWorkspaceTabs[objectKind] != TextureWorkspaceTab.OtherFiles) + { + _textureWorkspaceTabs[objectKind] = TextureWorkspaceTab.OtherFiles; + } + DrawOtherFileWorkspace(otherFileGroups); + } + } + } + + private void DrawTextureTabContent(ObjectKind objectKind) + { + var scale = ImGuiHelpers.GlobalScale; + var objectRows = _textureRows.Where(row => row.ObjectKind == objectKind).ToList(); + var hasAnyTextureRows = objectRows.Count > 0; + var availableCategories = objectRows.Select(row => row.Category) + .Distinct() + .OrderBy(c => c.ToString(), StringComparer.Ordinal) + .ToList(); + var availableSlots = objectRows + .Select(row => row.Slot) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) + .ToList(); + var availableMapKinds = objectRows.Select(row => row.MapKind) + .Distinct() + .OrderBy(m => m.ToString(), StringComparer.Ordinal) + .ToList(); + + var totalTextureCount = objectRows.Count; + var totalTextureOriginal = objectRows.Sum(row => row.OriginalSize); + var totalTextureCompressed = objectRows.Sum(row => row.CompressedSize); + + IEnumerable filtered = objectRows; + + if (_textureCategoryFilter is { } categoryFilter) + { + filtered = filtered.Where(row => row.Category == categoryFilter); + } + + if (!string.Equals(_textureSlotFilter, "All", StringComparison.Ordinal)) + { + filtered = filtered.Where(row => string.Equals(row.Slot, _textureSlotFilter, StringComparison.OrdinalIgnoreCase)); + } + + if (_textureMapFilter is { } mapFilter) + { + filtered = filtered.Where(row => row.MapKind == mapFilter); + } + + if (_textureTargetFilter is { } targetFilter) + { + filtered = filtered.Where(row => + (row.CurrentTarget != null && row.CurrentTarget == targetFilter) || + (row.CurrentTarget == null && row.SuggestedTarget == targetFilter)); + } + + if (!string.IsNullOrWhiteSpace(_textureSearch)) + { + var term = _textureSearch.Trim(); + filtered = filtered.Where(row => + row.DisplayName.Contains(term, StringComparison.OrdinalIgnoreCase) || + row.PrimaryGamePath.Contains(term, StringComparison.OrdinalIgnoreCase) || + row.Hash.Contains(term, StringComparison.OrdinalIgnoreCase)); + } + + var rows = filtered.ToList(); + + if (!string.IsNullOrEmpty(_selectedTextureKey) && rows.All(r => r.Key != _selectedTextureKey)) + { + _selectedTextureKey = rows.FirstOrDefault()?.Key ?? string.Empty; + } + + var totalOriginal = rows.Sum(r => r.OriginalSize); + var totalCompressed = rows.Sum(r => r.CompressedSize); + + var availableSize = ImGui.GetContentRegionAvail(); + var windowPos = ImGui.GetWindowPos(); + var spacingX = ImGui.GetStyle().ItemSpacing.X; + var filterSplitterWidth = TextureFilterSplitterWidth * scale; + var detailSplitterWidth = (_textureDetailCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth) * scale; + var totalSplitterWidth = filterSplitterWidth + detailSplitterWidth; + var totalSpacing = 2 * spacingX; + const float minFilterWidth = MinTextureFilterPaneWidth; + const float minDetailWidth = MinTextureDetailPaneWidth; + const float minCenterWidth = 340f; + + var detailMinForLayout = _textureDetailCollapsed ? 0f : minDetailWidth; + var dynamicFilterMax = Math.Max(minFilterWidth, availableSize.X - detailMinForLayout - minCenterWidth - totalSplitterWidth - totalSpacing); + var filterMaxBound = Math.Min(MaxTextureFilterPaneWidth, dynamicFilterMax); + var filterWidth = Math.Clamp(_textureFilterPaneWidth, minFilterWidth, filterMaxBound); + + var dynamicDetailMax = Math.Max(detailMinForLayout, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing); + var detailMaxBound = _textureDetailCollapsed ? 0f : Math.Min(MaxTextureDetailPaneWidth, dynamicDetailMax); + var detailWidth = _textureDetailCollapsed ? 0f : Math.Clamp(_textureDetailPaneWidth, minDetailWidth, detailMaxBound); + + var centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing; + + if (centerWidth < minCenterWidth) + { + var deficit = minCenterWidth - centerWidth; + if (!_textureDetailCollapsed) + { + detailWidth = Math.Clamp(detailWidth - deficit, minDetailWidth, + Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing))); + centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing; + if (centerWidth < minCenterWidth) + { + deficit = minCenterWidth - centerWidth; + filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth, + Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - detailWidth - minCenterWidth - totalSplitterWidth - totalSpacing))); + detailWidth = Math.Clamp(detailWidth, minDetailWidth, + Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing))); + centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing; + if (centerWidth < minCenterWidth) + { + centerWidth = minCenterWidth; + } + } + } + else + { + filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth, + Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - totalSplitterWidth - totalSpacing))); + centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing; + if (centerWidth < minCenterWidth) + { + centerWidth = minCenterWidth; + } + } + } + + _textureFilterPaneWidth = filterWidth; + if (!_textureDetailCollapsed) + { + _textureDetailPaneWidth = detailWidth; + } + + ImGui.BeginGroup(); + using (var filters = ImRaii.Child("textureFilters", new Vector2(filterWidth, 0), true)) + { + if (filters) + { + DrawTextureFilters( + availableCategories, + availableSlots, + availableMapKinds, + totalTextureCount, + totalTextureOriginal, + totalTextureCompressed); + } + } + ImGui.EndGroup(); + + var filterMin = ImGui.GetItemRectMin(); + var filterMax = ImGui.GetItemRectMax(); + var filterHeight = filterMax.Y - filterMin.Y; + var filterTopLocal = filterMin - windowPos; + var maxFilterResize = Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - detailMinForLayout - totalSplitterWidth - totalSpacing)); + DrawVerticalResizeHandle("##textureFilterSplitter", filterTopLocal.Y, filterHeight, ref _textureFilterPaneWidth, minFilterWidth, maxFilterResize, out _); + + TextureRow? selectedRow; + ImGui.BeginGroup(); + using (var tableChild = ImRaii.Child("textureTableArea", new Vector2(centerWidth, 0), false)) + { + selectedRow = DrawTextureTable(rows, totalOriginal, totalCompressed, hasAnyTextureRows); + } + ImGui.EndGroup(); + + var tableMin = ImGui.GetItemRectMin(); + var tableMax = ImGui.GetItemRectMax(); + var tableHeight = tableMax.Y - tableMin.Y; + var tableTopLocal = tableMin - windowPos; + var maxDetailResize = Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - _textureFilterPaneWidth - minCenterWidth - totalSplitterWidth - totalSpacing)); + var detailToggle = DrawVerticalResizeHandle( + "##textureDetailSplitter", + tableTopLocal.Y, + tableHeight, + ref _textureDetailPaneWidth, + minDetailWidth, + maxDetailResize, + out var detailDragging, + invert: true, + showToggle: true, + isCollapsed: _textureDetailCollapsed); + if (detailToggle) + { + _textureDetailCollapsed = !_textureDetailCollapsed; + } + if (_textureDetailCollapsed && detailDragging) + { + _textureDetailCollapsed = false; + } + + if (!_textureDetailCollapsed) + { + ImGui.BeginGroup(); + using (var detailChild = ImRaii.Child("textureDetailPane", new Vector2(detailWidth, 0), true)) + { + DrawTextureDetail(selectedRow); + } + ImGui.EndGroup(); + } + } + + private void DrawTextureFilters( + IReadOnlyList categories, + IReadOnlyList slots, + IReadOnlyList mapKinds, + int totalTextureCount, + long totalTextureOriginal, + long totalTextureCompressed) + { + var scale = ImGuiHelpers.GlobalScale; + var accent = UIColors.Get("LightlessBlue"); + var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.14f); + var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.35f); + var infoColor = ImGuiColors.DalamudGrey; + + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize))) + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale))) + using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg))) + using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder))) + { + var lineHeight = ImGui.GetTextLineHeightWithSpacing(); + var summaryHeight = MathF.Max(lineHeight * 2.4f, ImGui.GetFrameHeightWithSpacing() * 2.2f); + using (var totals = ImRaii.Child("textureTotalsSummary", new Vector2(-1f, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) + { + if (totals) + { + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(8f * scale, 4f * scale))) + { + if (ImGui.BeginTable("textureTotalsSummaryTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.NoHostExtendX)) + { + ImGui.TableNextRow(); + DrawSummaryCell(FontAwesomeIcon.Images, accent, $"{totalTextureCount:N0}", totalTextureCount == 1 ? "tex file" : "tex files", infoColor, scale); + DrawSummaryCell(FontAwesomeIcon.FileArchive, ImGuiColors.DalamudGrey, UiSharedService.ByteToString(totalTextureOriginal), "Actual size", infoColor, scale); + DrawSummaryCell(FontAwesomeIcon.CompressArrowsAlt, UIColors.Get("LightlessYellow2"), UiSharedService.ByteToString(totalTextureCompressed), "Compressed size", infoColor, scale); + ImGui.EndTable(); + } + } + } + } + } + + ImGuiHelpers.ScaledDummy(1); + + ImGui.TextUnformatted("Filters"); + ImGui.Separator(); + + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##textureSearch", "Search hash, path, or file name...", ref _textureSearch, 256); + + ImGuiHelpers.ScaledDummy(6); + + DrawEnumFilterCombo("Category", "All Categories", ref _textureCategoryFilter, categories); + DrawStringFilterCombo("Slot", ref _textureSlotFilter, slots, "All"); + DrawEnumFilterCombo("Map Type", "All Map Types", ref _textureMapFilter, mapKinds); + + if (_textureTargetFilter.HasValue && !_textureCompressionService.IsTargetSelectable(_textureTargetFilter.Value)) + { + _textureTargetFilter = null; + } + + DrawEnumFilterCombo("Compression", "Any Compression", ref _textureTargetFilter, _textureCompressionService.SelectableTargets); + + ImGuiHelpers.ScaledDummy(8); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Undo, "Reset filters")) + { + ResetTextureFilters(); + } + } + + private static void DrawEnumFilterCombo( + string label, + string allLabel, + ref T? currentSelection, + IEnumerable options) + where T : struct, Enum + { + var displayLabel = currentSelection?.ToString() ?? allLabel; + if (!ImGui.BeginCombo(label, displayLabel)) + { + return; + } + + bool noSelection = !currentSelection.HasValue; + if (ImGui.Selectable(allLabel, noSelection)) + { + currentSelection = null; + } + if (noSelection) + { + ImGui.SetItemDefaultFocus(); + } + + var comparer = EqualityComparer.Default; + foreach (var option in options) + { + bool selected = currentSelection.HasValue && comparer.Equals(currentSelection.Value, option); + if (ImGui.Selectable(option.ToString(), selected)) + { + currentSelection = option; + } + if (selected) + { + ImGui.SetItemDefaultFocus(); + } + } + + ImGui.EndCombo(); + } + + private static void DrawStringFilterCombo( + string label, + ref string currentSelection, + IEnumerable options, + string allLabel) + { + var displayLabel = string.IsNullOrEmpty(currentSelection) || string.Equals(currentSelection, allLabel, StringComparison.Ordinal) + ? allLabel + : currentSelection; + if (!ImGui.BeginCombo(label, displayLabel)) + { + return; + } + + bool allSelected = string.Equals(currentSelection, allLabel, StringComparison.Ordinal); + if (ImGui.Selectable(allLabel, allSelected)) + { + currentSelection = allLabel; + } + if (allSelected) + { + ImGui.SetItemDefaultFocus(); + } + + foreach (var option in options) + { + bool selected = string.Equals(currentSelection, option, StringComparison.OrdinalIgnoreCase); + if (ImGui.Selectable(option, selected)) + { + currentSelection = option; + } + if (selected) + { + ImGui.SetItemDefaultFocus(); + } + } + + ImGui.EndCombo(); + } + + private void DrawSummaryCell( + FontAwesomeIcon icon, + Vector4 iconColor, + string mainText, + string subText, + Vector4 subColor, + float scale, + string? extraText = null, + Vector4? extraColor = null, + string? tooltip = null) + { + ImGui.TableNextColumn(); + var spacing = new Vector2(6f * scale, 2f * scale); + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing)) + { + ImGui.BeginGroup(); + _uiSharedService.IconText(icon, iconColor); + ImGui.SameLine(0f, 4f * scale); + using (ImRaii.PushColor(ImGuiCol.Text, iconColor)) + { + ImGui.TextUnformatted(mainText); + } + using (ImRaii.PushColor(ImGuiCol.Text, subColor)) + { + ImGui.TextUnformatted(subText); + } + if (!string.IsNullOrWhiteSpace(extraText)) + { + ImGui.SameLine(0f, 4f * scale); + using (ImRaii.PushColor(ImGuiCol.Text, extraColor ?? subColor)) + { + ImGui.TextUnformatted(extraText); + } + } + ImGui.EndGroup(); + } + + if (!string.IsNullOrWhiteSpace(tooltip) && ImGui.IsItemHovered()) + { + ImGui.SetTooltip(tooltip); + } + } + + private void DrawOtherFileWorkspace(IReadOnlyList> otherFileGroups) + { + if (otherFileGroups.Count == 0) + { + return; + } + + var scale = ImGuiHelpers.GlobalScale; + + ImGuiHelpers.ScaledDummy(8); + var accent = UIColors.Get("LightlessBlue"); + var sectionAvail = ImGui.GetContentRegionAvail().Y; + IGrouping? activeGroup = null; + + using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize))) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(12f * scale, 4f * scale))) + using (var child = ImRaii.Child("otherFileTypes", new Vector2(-1f, sectionAvail - (SelectedFilePanelLogicalHeight * scale) - 12f * scale), true)) + { + if (child) + { + UiSharedService.ColorText("Other file types", UIColors.Get("LightlessPurple")); + ImGuiHelpers.ScaledDummy(4); + + using var tabBar = ImRaii.TabBar("otherFileTabs", ImGuiTabBarFlags.NoCloseWithMiddleMouseButton); + foreach (var fileGroup in otherFileGroups) + { + string tabLabel = $"{fileGroup.Key} [{fileGroup.Count()}]"; + var requiresCompute = fileGroup.Any(k => !k.IsComputed); + using var tabCol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(UIColors.Get("LightlessYellow")), requiresCompute); + if (requiresCompute) + { + tabLabel += " (!)"; + } + + ImRaii.IEndObject tabItem; + using (var textCol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(new Vector4(0, 0, 0, 1)), + requiresCompute && !string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal))) + { + tabItem = ImRaii.TabItem(tabLabel + "###other_" + fileGroup.Key); + } + + if (!tabItem) + { + tabItem.Dispose(); + continue; + } + + activeGroup = fileGroup; + + if (!string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal)) + { + _selectedFileTypeTab = fileGroup.Key; + _selectedHash = string.Empty; + } + + var originalTotal = fileGroup.Sum(c => c.OriginalSize); + var compressedTotal = fileGroup.Sum(c => c.CompressedSize); + + var badgeBg = new Vector4(accent.X, accent.Y, accent.Z, 0.18f); + var badgeBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.35f); + var summaryHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 2.6f, 36f * scale); + var summaryWidth = MathF.Min(420f * scale, ImGui.GetContentRegionAvail().X); + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 4f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize))) + using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(badgeBg))) + using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(badgeBorder))) + using (var summaryChild = ImRaii.Child($"otherFileSummary##{fileGroup.Key}", new Vector2(summaryWidth, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) + { + if (summaryChild) + { + var infoColor = ImGuiColors.DalamudGrey; + var countColor = UIColors.Get("LightlessBlue"); + var actualColor = ImGuiColors.DalamudGrey; + var compressedColor = UIColors.Get("LightlessYellow2"); + + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(10f * scale, 4f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(12f * scale, 2f * scale))) + { + using var summaryTable = ImRaii.Table("otherFileSummaryTable", 3, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoHostExtendX, + new Vector2(-1f, -1f)); + if (summaryTable) + { + ImGui.TableNextRow(); + DrawSummaryCell(FontAwesomeIcon.LayerGroup, countColor, + fileGroup.Count().ToString("N0", CultureInfo.InvariantCulture), + $"{fileGroup.Key} files", infoColor, scale); + DrawSummaryCell(FontAwesomeIcon.FileArchive, actualColor, + UiSharedService.ByteToString(originalTotal), + "Actual size", infoColor, scale); + DrawSummaryCell(FontAwesomeIcon.CompressArrowsAlt, compressedColor, + UiSharedService.ByteToString(compressedTotal), + "Compressed size", infoColor, scale); + } + } + } + } + + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, new Vector2(4f * scale, 3f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 3f * scale))) + { + DrawTable(fileGroup); + } + + tabItem.Dispose(); + } + } + } + + if (activeGroup == null && otherFileGroups.Count > 0) + { + activeGroup = otherFileGroups[0]; + } + + DrawSelectedFileDetails(activeGroup); + } + + private void DrawSelectedFileDetails(IGrouping? fileGroup) + { + var hasGroup = fileGroup != null; + var selectionInGroup = hasGroup && !string.IsNullOrEmpty(_selectedHash) && + fileGroup!.Any(entry => string.Equals(entry.Hash, _selectedHash, StringComparison.Ordinal)); + + var scale = ImGuiHelpers.GlobalScale; + var accent = UIColors.Get("LightlessBlue"); + var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.12f); + var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.3f); + + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize))) + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(10f * scale, 6f * scale))) + using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg))) + using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder))) + using (var child = ImRaii.Child("selectedFileDetails", new Vector2(-1f, SelectedFilePanelLogicalHeight * scale), true)) + { + if (!child) + { + return; + } + + if (!selectionInGroup || + _cachedAnalysis == null || + !_cachedAnalysis.TryGetValue(_selectedObjectTab, out var objectEntries) || + !objectEntries.TryGetValue(_selectedHash, out var item)) + { + UiSharedService.ColorText("Select a file row to view details.", ImGuiColors.DalamudGrey); + return; + } + + UiSharedService.ColorText("Selected file:", UIColors.Get("LightlessBlue")); + ImGui.SameLine(); + UiSharedService.ColorText(_selectedHash, UIColors.Get("LightlessYellow")); + + ImGuiHelpers.ScaledDummy(2); + + UiSharedService.ColorText("Local file path:", UIColors.Get("LightlessBlue")); + ImGui.SameLine(); + UiSharedService.TextWrapped(item.FilePaths[0]); + if (item.FilePaths.Count > 1) + { + ImGui.SameLine(); + ImGui.TextUnformatted($"(and {item.FilePaths.Count - 1} more)"); + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); + UiSharedService.AttachToolTip(string.Join(Environment.NewLine, item.FilePaths.Skip(1))); + } + + UiSharedService.ColorText("Used by game path:", UIColors.Get("LightlessBlue")); + ImGui.SameLine(); + UiSharedService.TextWrapped(item.GamePaths[0]); + if (item.GamePaths.Count > 1) + { + ImGui.SameLine(); + ImGui.TextUnformatted($"(and {item.GamePaths.Count - 1} more)"); + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); + UiSharedService.AttachToolTip(string.Join(Environment.NewLine, item.GamePaths.Skip(1))); + } + } + } + + private TextureRow? DrawTextureTable(IReadOnlyList rows, long totalOriginal, long totalCompressed, bool hasAnyTextureRows) + { + var scale = ImGuiHelpers.GlobalScale; + if (rows.Count > 0) + { + var accent = UIColors.Get("LightlessBlue"); + var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.14f); + var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.35f); + var originalColor = ImGuiColors.DalamudGrey; + var compressedColor = UIColors.Get("LightlessYellow2"); + var infoColor = ImGuiColors.DalamudGrey; + var countLabel = rows.Count == 1 ? "Matching texture" : "Matching textures"; + var diff = totalOriginal - totalCompressed; + var diffMagnitude = Math.Abs(diff); + var diffColor = diff > 0 ? UIColors.Get("LightlessGreen") + : diff < 0 ? UIColors.Get("DimRed") + : ImGuiColors.DalamudGrey; + var diffLabel = diff > 0 ? "Saved" : diff < 0 ? "Overhead" : "Lossless"; + var diffPercent = diffMagnitude > 0 && totalOriginal > 0 + ? ((diffMagnitude * 100d) / totalOriginal).ToString("0", CultureInfo.InvariantCulture) + "%" + : null; + + using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale)) + using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize))) + using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale))) + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(18f * scale, 4f * scale))) + using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg))) + using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder))) + { + var lineHeight = ImGui.GetTextLineHeightWithSpacing(); + var summaryHeight = MathF.Max(lineHeight * 2.4f, ImGui.GetFrameHeightWithSpacing() * 2.2f); + using (var summary = ImRaii.Child("textureCompressionSummary", new Vector2(-1f, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) + { + if (summary) + { + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(8f * scale, 4f * scale))) + { + if (ImGui.BeginTable("textureCompressionSummaryTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.NoHostExtendX)) + { + ImGui.TableNextRow(); + DrawSummaryStat(FontAwesomeIcon.Images, accent, $"{rows.Count:N0}", countLabel, infoColor); + DrawSummaryStat(FontAwesomeIcon.FileArchive, originalColor, UiSharedService.ByteToString(totalOriginal), "Original total", infoColor); + DrawSummaryStat(FontAwesomeIcon.CompressArrowsAlt, compressedColor, UiSharedService.ByteToString(totalCompressed), "Compressed total", infoColor); + DrawSummaryStat( + FontAwesomeIcon.ChartLine, + diffColor, + diffMagnitude > 0 ? UiSharedService.ByteToString(diffMagnitude) : "No change", + diffLabel, + diffColor, + diffPercent, + diffColor); + + ImGui.EndTable(); + } + } + } + } + } + } + else + { + if (hasAnyTextureRows) + { + UiSharedService.TextWrapped("No textures match the current filters. Try clearing filters or refreshing the analysis session."); + } + else + { + UiSharedService.ColorText("No textures recorded for this object.", ImGuiColors.DalamudGrey); + } + } + + UiSharedService.TextWrapped("Mark textures using the checkbox to add them to the compression queue. You can adjust the target format for each texture before starting the batch compression."); + + bool conversionRunning = _conversionTask != null && !_conversionTask.IsCompleted; + var activeSelectionCount = _textureRows.Count(row => _selectedTextureKeys.Contains(row.Key) && !row.IsAlreadyCompressed); + bool hasSelection = activeSelectionCount > 0; + using (ImRaii.Disabled(conversionRunning || !hasSelection)) + { + var label = hasSelection ? $"Compress {activeSelectionCount} selected" : "Compress selected"; + if (_uiSharedService.IconTextButton(FontAwesomeIcon.CompressArrowsAlt, label, 220f * scale)) + { + StartTextureConversion(); + } + } + ImGui.SameLine(); + using (ImRaii.Disabled(_selectedTextureKeys.Count == 0 && _textureSelections.Count == 0)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Clear marks", 160f * scale)) + { + _selectedTextureKeys.Clear(); + _textureSelections.Clear(); + } + } + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Refresh", 130f * scale)) + { + _textureRowsDirty = true; + } + + TextureRow? lastSelected = null; + using (var table = ImRaii.Table("textureDataTable", 9, + ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.Sortable, + new Vector2(-1, 0))) + { + if (table) + { + ImGui.TableSetupColumn("##select", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort, 32f * scale); + ImGui.TableSetupColumn("Texture", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort, 310f * scale); + ImGui.TableSetupColumn("Slot", ImGuiTableColumnFlags.NoSort); + ImGui.TableSetupColumn("Map", ImGuiTableColumnFlags.NoSort); + ImGui.TableSetupColumn("Format", ImGuiTableColumnFlags.NoSort); + ImGui.TableSetupColumn("Recommended", ImGuiTableColumnFlags.NoSort); + ImGui.TableSetupColumn("Target", ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort, 140f * scale); + ImGui.TableSetupColumn("Original", ImGuiTableColumnFlags.PreferSortDescending); + ImGui.TableSetupColumn("Compressed", ImGuiTableColumnFlags.PreferSortDescending); + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + + var targets = _textureCompressionService.SelectableTargets; + + IEnumerable orderedRows = rows; + var sortSpecs = ImGui.TableGetSortSpecs(); + if (sortSpecs.SpecsCount > 0) + { + var spec = sortSpecs.Specs[0]; + orderedRows = spec.ColumnIndex switch + { + 7 => spec.SortDirection == ImGuiSortDirection.Ascending + ? rows.OrderBy(r => r.OriginalSize) + : rows.OrderByDescending(r => r.OriginalSize), + 8 => spec.SortDirection == ImGuiSortDirection.Ascending + ? rows.OrderBy(r => r.CompressedSize) + : rows.OrderByDescending(r => r.CompressedSize), + _ => rows + }; + + sortSpecs.SpecsDirty = false; + } + + var index = 0; + foreach (var row in orderedRows) + { + DrawTextureRow(row, targets, index); + index++; + } + } + } + + return rows.FirstOrDefault(r => r.Key == _selectedTextureKey); + + void DrawSummaryStat(FontAwesomeIcon icon, Vector4 iconColor, string mainText, string subText, Vector4 subColor, string? extraText = null, Vector4? extraColor = null) + { + ImGui.TableNextColumn(); + using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(6f * scale, 2f * scale))) + using (ImRaii.Group()) + { + _uiSharedService.IconText(icon, iconColor); + ImGui.SameLine(0f, 6f * scale); + using (ImRaii.PushColor(ImGuiCol.Text, iconColor)) + { + ImGui.TextUnformatted(mainText); + } + using (ImRaii.PushColor(ImGuiCol.Text, subColor)) + { + ImGui.TextUnformatted(subText); + } + if (!string.IsNullOrWhiteSpace(extraText)) + { + ImGui.SameLine(0f, 6f * scale); + using (ImRaii.PushColor(ImGuiCol.Text, extraColor ?? iconColor)) + { + ImGui.TextUnformatted(extraText); + } + } + } + } + } + private void StartTextureConversion() + { + if (_conversionTask != null && !_conversionTask.IsCompleted) + { + return; + } + + var selectedRows = _textureRows + .Where(row => _selectedTextureKeys.Contains(row.Key) && !row.IsAlreadyCompressed) + .ToList(); + if (selectedRows.Count == 0) + { + return; + } + + var requests = selectedRows.Select(row => + { + var desiredTarget = _textureSelections.TryGetValue(row.Key, out var selection) + ? selection + : row.SuggestedTarget ?? row.CurrentTarget ?? _textureCompressionService.DefaultTarget; + var normalizedTarget = _textureCompressionService.NormalizeTarget(desiredTarget); + _textureSelections[row.Key] = normalizedTarget; + + return new TextureCompressionRequest(row.PrimaryFilePath, row.DuplicateFilePaths, normalizedTarget); + }).ToList(); + + _conversionCancellationTokenSource = _conversionCancellationTokenSource.CancelRecreate(); + _conversionTotalJobs = requests.Count; + _lastConversionProgress = null; + _conversionCurrentFileName = string.Empty; + _conversionCurrentFileProgress = 0; + _conversionFailed = false; + + _conversionTask = RunTextureConversionAsync(requests, _conversionCancellationTokenSource.Token); + _showModal = true; + } + + private async Task RunTextureConversionAsync(List requests, CancellationToken token) + { + try + { + await _textureCompressionService.ConvertTexturesAsync(requests, _conversionProgress, token).ConfigureAwait(false); + + if (!token.IsCancellationRequested) + { + var affectedPaths = requests + .SelectMany(static request => + { + IEnumerable paths = request.DuplicateFilePaths; + return new[] { request.PrimaryFilePath }.Concat(paths); + }) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (affectedPaths.Count > 0) + { + try + { + await _characterAnalyzer.UpdateFileEntriesAsync(affectedPaths, token).ConfigureAwait(false); + _hasUpdate = true; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception updateEx) + { + _logger.LogWarning(updateEx, "Failed to refresh compressed size data after texture conversion."); + } + } + } + } + catch (OperationCanceledException) + { + _logger.LogInformation("Texture compression was cancelled."); + } + catch (Exception ex) + { + _conversionFailed = true; + _logger.LogError(ex, "Texture compression failed."); + } + finally + { + _selectedTextureKeys.Clear(); + _textureSelections.Clear(); + _textureRowsDirty = true; + } + } + + private bool DrawVerticalResizeHandle( + string id, + float topY, + float height, + ref float leftWidth, + float minWidth, + float maxWidth, + out bool isDragging, + bool invert = false, + bool showToggle = false, + bool isCollapsed = false) + { + var scale = ImGuiHelpers.GlobalScale; + var splitterWidth = (showToggle + ? (isCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth) + : TextureFilterSplitterWidth) * scale; + ImGui.SameLine(); + var cursor = ImGui.GetCursorPos(); + var contentMin = ImGui.GetWindowContentRegionMin(); + var contentMax = ImGui.GetWindowContentRegionMax(); + var clampedTop = MathF.Max(topY, contentMin.Y); + var clampedBottom = MathF.Min(topY + height, contentMax.Y); + var clampedHeight = MathF.Max(0f, clampedBottom - clampedTop); + var splitterRounding = ImGui.GetStyle().FrameRounding; + ImGui.SetCursorPos(new Vector2(cursor.X, clampedTop)); + if (clampedHeight <= 0f) + { + isDragging = false; + ImGui.SetCursorPos(new Vector2(cursor.X + splitterWidth + ImGui.GetStyle().ItemSpacing.X, cursor.Y)); + return false; + } + + ImGui.InvisibleButton(id, new Vector2(splitterWidth, clampedHeight)); + var drawList = ImGui.GetWindowDrawList(); + var rectMin = ImGui.GetItemRectMin(); + var rectMax = ImGui.GetItemRectMax(); + var windowPos = ImGui.GetWindowPos(); + var clipMin = windowPos + contentMin; + var clipMax = windowPos + contentMax; + drawList.PushClipRect(clipMin, clipMax, true); + var clipInset = 1f * scale; + var drawMin = new Vector2( + MathF.Max(rectMin.X, clipMin.X), + MathF.Max(rectMin.Y, clipMin.Y)); + var drawMax = new Vector2( + MathF.Min(rectMax.X, clipMax.X - clipInset), + MathF.Min(rectMax.Y, clipMax.Y)); + var hovered = ImGui.IsItemHovered(); + isDragging = ImGui.IsItemActive(); + var baseColor = UIColors.Get("ButtonDefault"); + var hoverColor = UIColors.Get("LightlessPurple"); + var activeColor = UIColors.Get("LightlessPurpleActive"); + var handleColor = isDragging ? activeColor : hovered ? hoverColor : baseColor; + drawList.AddRectFilled(drawMin, drawMax, UiSharedService.Color(handleColor), splitterRounding); + drawList.AddRect(drawMin, drawMax, UiSharedService.Color(new Vector4(1f, 1f, 1f, 0.12f)), splitterRounding); + + bool toggleHovered = false; + bool toggleClicked = false; + if (showToggle) + { + var icon = isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft; + Vector2 iconSize; + using (_uiSharedService.IconFont.Push()) + { + iconSize = ImGui.CalcTextSize(icon.ToIconString()); + } + + var toggleHeight = MathF.Min(clampedHeight, 64f * scale); + var toggleMin = new Vector2( + drawMin.X, + drawMin.Y + (drawMax.Y - drawMin.Y - toggleHeight) / 2f); + var toggleMax = new Vector2( + drawMax.X, + toggleMin.Y + toggleHeight); + var toggleColorBase = UIColors.Get("LightlessPurple"); + toggleHovered = ImGui.IsMouseHoveringRect(toggleMin, toggleMax); + var toggleBg = toggleHovered + ? new Vector4(toggleColorBase.X, toggleColorBase.Y, toggleColorBase.Z, 0.65f) + : new Vector4(toggleColorBase.X, toggleColorBase.Y, toggleColorBase.Z, 0.35f); + if (toggleHovered) + { + UiSharedService.AttachToolTip(isCollapsed ? "Show texture details." : "Hide texture details."); + } + + drawList.AddRectFilled(toggleMin, toggleMax, UiSharedService.Color(toggleBg), splitterRounding); + drawList.AddRect(toggleMin, toggleMax, UiSharedService.Color(toggleColorBase), splitterRounding); + + var iconPos = new Vector2( + drawMin.X + (drawMax.X - drawMin.X - iconSize.X) / 2f, + drawMin.Y + (drawMax.Y - drawMin.Y - iconSize.Y) / 2f); + using (_uiSharedService.IconFont.Push()) + { + drawList.AddText(iconPos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString()); + } + + if (toggleHovered && ImGui.IsMouseReleased(ImGuiMouseButton.Left) && !ImGui.IsMouseDragging(ImGuiMouseButton.Left)) + { + toggleClicked = true; + } + } + + if (isDragging && !toggleHovered) + { + var delta = ImGui.GetIO().MouseDelta.X / scale; + leftWidth += invert ? -delta : delta; + leftWidth = Math.Clamp(leftWidth, minWidth, maxWidth); + } + + drawList.PopClipRect(); + + ImGui.SetCursorPos(new Vector2(cursor.X + splitterWidth + ImGui.GetStyle().ItemSpacing.X, cursor.Y)); + return toggleClicked; + } + + private (IDalamudTextureWrap? Texture, bool IsLoading, string? Error) GetTexturePreview(TextureRow row) + { + var key = row.Key; + if (!_texturePreviews.TryGetValue(key, out var state)) + { + state = new TexturePreviewState(); + _texturePreviews[key] = state; + } + + state.LastAccessUtc = DateTime.UtcNow; + + if (state.Texture != null) + { + return (state.Texture, false, state.ErrorMessage); + } + + if (state.LoadTask == null) + { + state.LoadTask = Task.Run(async () => + { + try + { + var texture = await BuildPreviewAsync(row, CancellationToken.None).ConfigureAwait(false); + state.Texture = texture; + state.ErrorMessage = texture == null ? "Preview unavailable." : null; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to load preview for {File}", row.PrimaryFilePath); + state.ErrorMessage = "Failed to load preview."; + } + }); + } + + if (state.LoadTask.IsCompleted) + { + state.LoadTask = null; + return (state.Texture, false, state.ErrorMessage); + } + + return (null, true, state.ErrorMessage); + } + + private async Task BuildPreviewAsync(TextureRow row, CancellationToken token) + { + const int PreviewMaxDimension = 1024; + + token.ThrowIfCancellationRequested(); + + if (!File.Exists(row.PrimaryFilePath)) + { + return null; + } + + try + { + using var scratch = TexFileHelper.Load(row.PrimaryFilePath); + using var rgbaScratch = scratch.GetRGBA(out var rgbaInfo).ThrowIfError(rgbaInfo); + + var meta = rgbaInfo.Meta; + var width = meta.Width; + var height = meta.Height; + var bytesPerPixel = meta.Format.BitsPerPixel() / 8; + var requiredLength = width * height * bytesPerPixel; + + token.ThrowIfCancellationRequested(); + + var rgbaPixels = rgbaScratch.Pixels[..requiredLength].ToArray(); + using var image = ImageSharpImage.LoadPixelData(rgbaPixels, width, height); + + if (Math.Max(width, height) > PreviewMaxDimension) + { + var dominant = Math.Max(width, height); + var scale = PreviewMaxDimension / (float)dominant; + var targetWidth = Math.Max(1, (int)MathF.Round(width * scale)); + var targetHeight = Math.Max(1, (int)MathF.Round(height * scale)); + image.Mutate(ctx => ctx.Resize(targetWidth, targetHeight, KnownResamplers.Lanczos3)); + } + + using var ms = new MemoryStream(); + await image.SaveAsPngAsync(ms, cancellationToken: token).ConfigureAwait(false); + return _uiSharedService.LoadImage(ms.ToArray()); + } + catch (OperationCanceledException) + { + return null; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Preview generation failed for {File}", row.PrimaryFilePath); + return null; + } + } + + private void ResetPreview(string key) + { + if (_texturePreviews.TryGetValue(key, out var state)) + { + state.Texture?.Dispose(); + _texturePreviews.Remove(key); + } + } + + private void DrawTextureRow(TextureRow row, IReadOnlyList targets, int index) + { + var key = row.Key; + bool isSelected = string.Equals(_selectedTextureKey, key, StringComparison.Ordinal); + + ImGui.TableNextRow(ImGuiTableRowFlags.None, 0); + ApplyTextureRowBackground(row, isSelected); + + bool canCompress = !row.IsAlreadyCompressed; + if (!canCompress) + { + _selectedTextureKeys.Remove(key); + _textureSelections.Remove(key); + } + + ImGui.TableNextColumn(); + if (canCompress) + { + bool marked = _selectedTextureKeys.Contains(key); + if (UiSharedService.CheckboxWithBorder($"##select{index}", ref marked, UIColors.Get("LightlessPurple"), 1.5f)) + { + if (marked) + { + _selectedTextureKeys.Add(key); + } + else + { + _selectedTextureKeys.Remove(key); + } + } + UiSharedService.AttachToolTip("Mark texture for batch compression."); + } + else + { + _uiSharedService.IconText(FontAwesomeIcon.Check, ImGuiColors.DalamudWhite); + UiSharedService.AttachToolTip("Already stored in a compressed format; additional compression is disabled."); + } + + DrawSelectableColumn(isSelected, () => + { + var selectableLabel = $"{row.DisplayName}##texName{index}"; + if (ImGui.Selectable(selectableLabel, isSelected)) + { + _selectedTextureKey = isSelected ? string.Empty : key; + } + + return () => UiSharedService.AttachToolTip($"{row.PrimaryFilePath}{UiSharedService.TooltipSeparator}{string.Join(Environment.NewLine, row.GamePaths)}"); + }); + + DrawSelectableColumn(isSelected, () => + { + ImGui.TextUnformatted(row.Slot); + return null; + }); + DrawSelectableColumn(isSelected, () => + { + ImGui.TextUnformatted(row.MapKind.ToString()); + return null; + }); + DrawSelectableColumn(isSelected, () => + { + Action? tooltipAction = null; + ImGui.TextUnformatted(row.Format); + if (!row.IsAlreadyCompressed) + { + ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale); + var iconColor = isSelected ? SelectedTextureRowTextColor : UIColors.Get("LightlessYellow"); + _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, iconColor); + tooltipAction = () => UiSharedService.AttachToolTip("Run compression to reduce file size."); + } + return tooltipAction; + }); + + DrawSelectableColumn(isSelected, () => + { + if (row.SuggestedTarget.HasValue) + { + ImGui.TextUnformatted(row.SuggestedTarget.Value.ToString()); + if (!string.IsNullOrEmpty(row.SuggestionReason)) + { + var reason = row.SuggestionReason; + return () => UiSharedService.AttachToolTip(reason); + } + } + else + { + ImGui.TextUnformatted("-"); + } + + return null; + }); + + ImGui.TableNextColumn(); + if (canCompress) + { + var desiredTarget = _textureSelections.TryGetValue(key, out var storedTarget) + ? storedTarget + : row.SuggestedTarget ?? row.CurrentTarget ?? _textureCompressionService.DefaultTarget; + var currentSelection = _textureCompressionService.NormalizeTarget(desiredTarget); + if (!_textureSelections.TryGetValue(key, out _) || storedTarget != currentSelection) + { + _textureSelections[key] = currentSelection; + } + var comboLabel = currentSelection.ToString(); + ImGui.SetNextItemWidth(-1); + if (ImGui.BeginCombo($"##target{index}", comboLabel)) + { + foreach (var target in targets) + { + bool targetSelected = currentSelection == target; + if (ImGui.Selectable(target.ToString(), targetSelected)) + { + _textureSelections[key] = target; + currentSelection = target; + } + if (TextureMetadataHelper.TryGetRecommendationInfo(target, out var targetInfo)) + { + UiSharedService.AttachToolTip($"{targetInfo.Title}{UiSharedService.TooltipSeparator}{targetInfo.Description}"); + } + if (targetSelected) + { + ImGui.SetItemDefaultFocus(); + } + } + + ImGui.EndCombo(); + } + } + else + { + var label = row.CurrentTarget?.ToString() ?? row.Format; + ImGui.TextUnformatted(label); + UiSharedService.AttachToolTip("This texture is already compressed and cannot be processed again."); + } + + DrawSelectableColumn(isSelected, () => + { + ImGui.TextUnformatted(UiSharedService.ByteToString(row.OriginalSize)); + return null; + }); + DrawSelectableColumn(isSelected, () => + { + ImGui.TextUnformatted(UiSharedService.ByteToString(row.CompressedSize)); + return null; + }); + } + + private static void DrawSelectableColumn(bool isSelected, Func draw) + { + ImGui.TableNextColumn(); + if (isSelected) + { + ImGui.PushStyleColor(ImGuiCol.Text, SelectedTextureRowTextColor); + } + + var after = draw(); + + if (isSelected) + { + ImGui.PopStyleColor(); + } + + after?.Invoke(); + } + + private static void ApplyTextureRowBackground(TextureRow row, bool isSelected) + { + if (isSelected) + { + var highlight = UiSharedService.Color(UIColors.Get("LightlessYellow")); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, highlight); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, highlight); + } + else if (row.IsAlreadyCompressed) + { + var compressedColor = UiSharedService.Color(UIColors.Get("LightlessGreenDefault")); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, compressedColor); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, compressedColor); + } + else if (!row.IsComputed) + { + var warning = UiSharedService.Color(UIColors.Get("DimRed")); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, warning); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, warning); + } + } + + private void DrawTextureDetail(TextureRow? row) + { + var scale = ImGuiHelpers.GlobalScale; + + UiSharedService.ColorText("Texture Details", UIColors.Get("LightlessPurple")); + if (row != null) + { + ImGui.SameLine(); + ImGui.TextUnformatted(row.DisplayName); + UiSharedService.AttachToolTip("Source file: " + row.PrimaryFilePath); + } + ImGui.Separator(); + + if (row == null) + { + UiSharedService.ColorText("Select a texture to view details.", ImGuiColors.DalamudGrey); + return; + } + + var (previewTexture, previewLoading, previewError) = GetTexturePreview(row); + var previewSize = new Vector2(_texturePreviewSize * 0.85f, _texturePreviewSize * 0.85f) * scale; + + if (previewTexture != null) + { + ImGui.Image(previewTexture.Handle, previewSize); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.SyncAlt, "Refresh preview", 180f * ImGuiHelpers.GlobalScale)) + { + ResetPreview(row.Key); + } + } + else + { + using (ImRaii.Child("previewPlaceholder", previewSize, true)) + { + UiSharedService.TextWrapped(previewLoading ? "Generating preview..." : previewError ?? "Preview unavailable."); + } + if (!previewLoading && _uiSharedService.IconTextButton(FontAwesomeIcon.SyncAlt, "Retry preview", 180f * ImGuiHelpers.GlobalScale)) + { + ResetPreview(row.Key); + } + } + + var desiredDetailTarget = _textureSelections.TryGetValue(row.Key, out var userTarget) + ? userTarget + : row.SuggestedTarget ?? row.CurrentTarget ?? _textureCompressionService.DefaultTarget; + var selectedTarget = _textureCompressionService.NormalizeTarget(desiredDetailTarget); + if (!_textureSelections.TryGetValue(row.Key, out _) || userTarget != selectedTarget) + { + _textureSelections[row.Key] = selectedTarget; + } + var hasSelectedInfo = TextureMetadataHelper.TryGetRecommendationInfo(selectedTarget, out var selectedInfo); + + using (ImRaii.Child("textureDetailInfo", new Vector2(-1, 0), true, ImGuiWindowFlags.AlwaysVerticalScrollbar)) + { + using var detailSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(6f * scale, 2f * scale)); + DrawMetaOverview(); + + ImGuiHelpers.ScaledDummy(4); + DrawSizeSummary(); + + ImGuiHelpers.ScaledDummy(4); + DrawCompressionInsights(); + + ImGuiHelpers.ScaledDummy(6); + DrawExpandableList("Duplicate Files", row.DuplicateFilePaths, "No duplicate files detected."); + DrawExpandableList("Game Paths", row.GamePaths, "No game paths recorded."); + } + + void DrawMetaOverview() + { + var labelColor = ImGuiColors.DalamudGrey; + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(5f * scale, 2f * scale))) + { + var metaFlags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX; + if (ImGui.BeginTable("textureMetaOverview", 2, metaFlags)) + { + MetaRow(FontAwesomeIcon.Cube, "Object", row.ObjectKind.ToString()); + MetaRow(FontAwesomeIcon.Tag, "Slot", row.Slot); + MetaRow(FontAwesomeIcon.LayerGroup, "Map Type", row.MapKind.ToString()); + MetaRow(FontAwesomeIcon.Fingerprint, "Hash", row.Hash, UIColors.Get("LightlessBlue")); + MetaRow(FontAwesomeIcon.InfoCircle, "Current Format", row.Format); + + var selectedLabel = hasSelectedInfo ? selectedInfo!.Title : selectedTarget.ToString(); + var selectionColor = hasSelectedInfo ? UIColors.Get("LightlessYellow") : UIColors.Get("LightlessGreen"); + MetaRow(FontAwesomeIcon.Bullseye, "Selected Target", selectedLabel, selectionColor); + + ImGui.EndTable(); + } + } + + void MetaRow(FontAwesomeIcon icon, string label, string value, Vector4? valueColor = null) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + _uiSharedService.IconText(icon, labelColor); + ImGui.SameLine(0f, 4f * scale); + using (ImRaii.PushColor(ImGuiCol.Text, labelColor)) + { + ImGui.TextUnformatted(label); + } + + ImGui.TableNextColumn(); + if (valueColor.HasValue) + { + using (ImRaii.PushColor(ImGuiCol.Text, valueColor.Value)) + { + ImGui.TextUnformatted(value); + } + } + else + { + ImGui.TextUnformatted(value); + } + } + } + + void DrawSizeSummary() + { + var savedBytes = row.OriginalSize - row.CompressedSize; + var savedMagnitude = Math.Abs(savedBytes); + var savedColor = savedBytes > 0 ? UIColors.Get("LightlessGreen") : savedBytes < 0 ? UIColors.Get("DimRed") : ImGuiColors.DalamudGrey; + var savedLabel = savedBytes > 0 ? "Saved" : savedBytes < 0 ? "Over" : "Delta"; + var savedPercent = row.OriginalSize > 0 && savedMagnitude > 0 + ? $"{savedMagnitude * 100d / row.OriginalSize:0.#}%" + : null; + + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) + { + var statFlags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX; + if (ImGui.BeginTable("textureSizeSummary", 3, statFlags)) + { + ImGui.TableNextRow(); + StatCell(FontAwesomeIcon.Database, ImGuiColors.DalamudGrey, UiSharedService.ByteToString(row.OriginalSize), "Original"); + StatCell(FontAwesomeIcon.CompressArrowsAlt, UIColors.Get("LightlessYellow2"), UiSharedService.ByteToString(row.CompressedSize), "Compressed"); + StatCell(FontAwesomeIcon.ChartLine, savedColor, savedMagnitude > 0 ? UiSharedService.ByteToString(savedMagnitude) : "No change", savedLabel, savedPercent, savedColor); + ImGui.EndTable(); + } + } + + void StatCell(FontAwesomeIcon icon, Vector4 iconColor, string mainText, string caption, string? extra = null, Vector4? extraColor = null) + { + ImGui.TableNextColumn(); + _uiSharedService.IconText(icon, iconColor); + ImGui.SameLine(0f, 4f * scale); + using (ImRaii.PushColor(ImGuiCol.Text, iconColor)) + { + ImGui.TextUnformatted(mainText); + } + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) + { + ImGui.TextUnformatted(caption); + } + if (!string.IsNullOrEmpty(extra)) + { + ImGui.SameLine(0f, 4f * scale); + using (ImRaii.PushColor(ImGuiCol.Text, extraColor ?? iconColor)) + { + ImGui.TextUnformatted(extra); + } + } + } + } + + void DrawCompressionInsights() + { + var matchesRecommendation = row.SuggestedTarget.HasValue && row.SuggestedTarget.Value == selectedTarget; + var columnCount = row.SuggestedTarget.HasValue ? 2 : 1; + using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) + { + var cardFlags = ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX; + if (ImGui.BeginTable("textureCompressionCards", columnCount, cardFlags)) + { + ImGui.TableNextRow(); + var selectedTitleColor = matchesRecommendation ? UIColors.Get("LightlessGreen") : UIColors.Get("LightlessBlue"); + var selectedDescription = hasSelectedInfo + ? selectedInfo!.Description + : matchesRecommendation + ? "Selected target matches the automatic recommendation." + : "Manual selection without additional guidance."; + DrawCompressionCard("Selected Target", selectedLabelText(), selectedTitleColor, selectedDescription); + + if (row.SuggestedTarget.HasValue) + { + var recommendedTarget = row.SuggestedTarget.Value; + var hasRecommendationInfo = TextureMetadataHelper.TryGetRecommendationInfo(recommendedTarget, out var recommendedInfo); + var recommendedTitle = hasRecommendationInfo ? recommendedInfo!.Title : recommendedTarget.ToString(); + var recommendedDescription = hasRecommendationInfo + ? recommendedInfo!.Description + : string.IsNullOrEmpty(row.SuggestionReason) ? "No additional context provided." : row.SuggestionReason; + + DrawCompressionCard("Recommended Target", recommendedTitle, UIColors.Get("LightlessYellow"), recommendedDescription); + } + + ImGui.EndTable(); + } + } + + using (ImRaii.PushIndent(12f * scale)) + { + if (!row.SuggestedTarget.HasValue) + { + UiSharedService.ColorTextWrapped("No automatic recommendation available.", UIColors.Get("LightlessYellow")); + } + + if (!matchesRecommendation && row.SuggestedTarget.HasValue) + { + UiSharedService.ColorTextWrapped("Selected compression differs from the recommendation. Review before running batch conversion.", UIColors.Get("DimRed")); + } + } + + string selectedLabelText() + { + if (hasSelectedInfo) + { + return selectedInfo!.Title; + } + + return selectedTarget.ToString(); + } + + void DrawCompressionCard(string header, string title, Vector4 titleColor, string body) + { + ImGui.TableNextColumn(); + UiSharedService.ColorText(header, UIColors.Get("LightlessPurple")); + using (ImRaii.PushColor(ImGuiCol.Text, titleColor)) + { + ImGui.TextUnformatted(title); + } + UiSharedService.TextWrapped(body, 0, ImGuiColors.DalamudGrey); + } + } + + void DrawExpandableList(string title, IReadOnlyList entries, string emptyMessage) + { + _ = emptyMessage; + var count = entries.Count; + using var headerDefault = ImRaii.PushColor(ImGuiCol.Header, UiSharedService.Color(new Vector4(0.15f, 0.15f, 0.18f, 0.95f))); + using var headerHover = ImRaii.PushColor(ImGuiCol.HeaderHovered, UiSharedService.Color(new Vector4(0.2f, 0.2f, 0.25f, 1f))); + using var headerActive = ImRaii.PushColor(ImGuiCol.HeaderActive, UiSharedService.Color(new Vector4(0.25f, 0.25f, 0.3f, 1f))); + var label = $"{title} ({count})"; + if (!ImGui.CollapsingHeader(label, count == 0 ? ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.None)) + { + return; + } + + if (count == 0) + { + return; + } + + var tableFlags = ImGuiTableFlags.PadOuterX | ImGuiTableFlags.NoHostExtendX | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.BordersOuter; + if (ImGui.BeginTable($"{title}Table", 2, tableFlags)) + { + ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 28f * scale); + ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + + for (int i = 0; i < entries.Count; i++) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{i + 1}."); + + ImGui.TableNextColumn(); + var wrapPos = ImGui.GetCursorPosX() + ImGui.GetColumnWidth(); + ImGui.PushTextWrapPos(wrapPos); + ImGui.TextUnformatted(entries[i]); + ImGui.PopTextWrapPos(); + } + + ImGui.EndTable(); + } + } + } + + private void SortCachedAnalysis( + ObjectKind objectKind, + Func, TKey> selector, + bool ascending, + IComparer? comparer = null) + { + if (_cachedAnalysis == null || !_cachedAnalysis.TryGetValue(objectKind, out var current)) + { + return; + } + + var ordered = ascending + ? (comparer != null ? current.OrderBy(selector, comparer) : current.OrderBy(selector)) + : (comparer != null ? current.OrderByDescending(selector, comparer) : current.OrderByDescending(selector)); + + _cachedAnalysis[objectKind] = ordered.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal); } private void DrawTable(IGrouping fileGroup) { - var tableColumns = string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) - ? (_enableBc7ConversionMode ? 7 : 6) - : (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) ? 6 : 5); - using var table = ImRaii.Table("Analysis", tableColumns, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, - new Vector2(0, 300)); - if (!table.Success) return; + var tableColumns = string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) ? 6 : 5; + var scale = ImGuiHelpers.GlobalScale; + using var table = ImRaii.Table("Analysis", tableColumns, + ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.BordersInnerV, + new Vector2(-1f, 0f)); + if (!table.Success) + { + return; + } + ImGui.TableSetupColumn("Hash"); ImGui.TableSetupColumn("Filepaths"); ImGui.TableSetupColumn("Gamepaths"); ImGui.TableSetupColumn("Original Size"); ImGui.TableSetupColumn("Compressed Size"); - if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal)) - { - ImGui.TableSetupColumn("Format"); - if (_enableBc7ConversionMode) ImGui.TableSetupColumn("Convert to BC7"); - } if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal)) { ImGui.TableSetupColumn("Triangles"); } + ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableHeadersRow(); var sortSpecs = ImGui.TableGetSortSpecs(); - if (sortSpecs.SpecsDirty) + if (sortSpecs.SpecsDirty && sortSpecs.SpecsCount > 0) { - var idx = sortSpecs.Specs.ColumnIndex; + var spec = sortSpecs.Specs[0]; + bool ascending = spec.SortDirection == ImGuiSortDirection.Ascending; - if (idx == 0 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Key, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (idx == 0 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Key, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.FilePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.FilePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (idx == 2 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.GamePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (idx == 2 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.GamePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (idx == 3 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.OriginalSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (idx == 3 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.OriginalSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.Triangles).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Triangles).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.Format.Value, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); - if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) - _cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Format.Value, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + switch (spec.ColumnIndex) + { + case 0: + SortCachedAnalysis(_selectedObjectTab, pair => pair.Key, ascending, StringComparer.Ordinal); + break; + case 1: + SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.FilePaths.Count, ascending); + break; + case 2: + SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.GamePaths.Count, ascending); + break; + case 3: + SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.OriginalSize, ascending); + break; + case 4: + SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.CompressedSize, ascending); + break; + case 5 when string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal): + SortCachedAnalysis(_selectedObjectTab, pair => pair.Value.Triangles, ascending); + break; + } sortSpecs.SpecsDirty = false; } foreach (var item in fileGroup) { - using var text = ImRaii.PushColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1), string.Equals(item.Hash, _selectedHash, StringComparison.Ordinal)); - using var text2 = ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 1, 1, 1), !item.IsComputed); + using var textColor = ImRaii.PushColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1), string.Equals(item.Hash, _selectedHash, StringComparison.Ordinal)); + using var missingColor = ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 1, 1, 1), !item.IsComputed); ImGui.TableNextColumn(); if (!item.IsComputed) { - ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudRed)); - ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudRed)); + var warning = UiSharedService.Color(UIColors.Get("DimRed")); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, warning); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, warning); } if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal)) { - ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(UIColors.Get("LightlessYellow"))); - ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(UIColors.Get("LightlessYellow"))); + var highlight = UiSharedService.Color(UIColors.Get("LightlessYellow")); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, highlight); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, highlight); } ImGui.TextUnformatted(item.Hash); - if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + if (ImGui.IsItemClicked()) + { + _selectedHash = string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal) + ? string.Empty + : item.Hash; + } + ImGui.TableNextColumn(); ImGui.TextUnformatted(item.FilePaths.Count.ToString()); - if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + ImGui.TableNextColumn(); ImGui.TextUnformatted(item.GamePaths.Count.ToString()); - if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + ImGui.TableNextColumn(); ImGui.TextUnformatted(UiSharedService.ByteToString(item.OriginalSize)); - if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + ImGui.TableNextColumn(); ImGui.TextUnformatted(UiSharedService.ByteToString(item.CompressedSize)); - if (ImGui.IsItemClicked()) _selectedHash = item.Hash; - if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal)) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(item.Format.Value); - if (ImGui.IsItemClicked()) _selectedHash = item.Hash; - if (_enableBc7ConversionMode) - { - ImGui.TableNextColumn(); - if (string.Equals(item.Format.Value, "BC7", StringComparison.Ordinal)) - { - ImGui.TextUnformatted(""); - continue; - } - var filePath = item.FilePaths[0]; - bool toConvert = _texturesToConvert.ContainsKey(filePath); - if (UiSharedService.CheckboxWithBorder("###convert" + item.Hash, ref toConvert, UIColors.Get("LightlessPurple"), 1.5f)) - { - if (toConvert && !_texturesToConvert.ContainsKey(filePath)) - { - _texturesToConvert[filePath] = item.FilePaths.Skip(1).ToArray(); - } - else if (!toConvert && _texturesToConvert.ContainsKey(filePath)) - { - _texturesToConvert.Remove(filePath); - } - } - } - } + if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal)) { ImGui.TableNextColumn(); ImGui.TextUnformatted(item.Triangles.ToString()); - if (ImGui.IsItemClicked()) _selectedHash = item.Hash; } } } -} \ No newline at end of file +} diff --git a/LightlessSync/UI/DownloadUi.cs b/LightlessSync/UI/DownloadUi.cs index aa3132a..b960b46 100644 --- a/LightlessSync/UI/DownloadUi.cs +++ b/LightlessSync/UI/DownloadUi.cs @@ -1,10 +1,11 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Colors; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Services.PairProcessing; using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files.Models; using Microsoft.Extensions.Logging; @@ -22,6 +23,10 @@ public class DownloadUi : WindowMediatorSubscriberBase private readonly UiSharedService _uiShared; private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly ConcurrentDictionary _uploadingPlayers = new(); + private readonly Dictionary _smoothed = []; + private readonly Dictionary _downloadSpeeds = []; + + private byte _transferBoxTransparency = 100; private bool _notificationDismissed = true; private int _lastDownloadStateHash = 0; @@ -95,155 +100,32 @@ public class DownloadUi : WindowMediatorSubscriberBase if (_configService.Current.ShowTransferWindow) { var limiterSnapshot = _pairProcessingLimiter.GetSnapshot(); - - try + + // Check if download notifications are enabled (not set to TextOverlay) + var useNotifications = + _configService.Current.UseLightlessNotifications && + _configService.Current.LightlessDownloadNotification == NotificationLocation.LightlessUi; + + if (useNotifications) { - if (_fileTransferManager.IsUploading) + if (!_currentDownloads.IsEmpty) { - var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot(); - var totalUploads = currentUploads.Count; - - var doneUploads = currentUploads.Count(c => c.IsTransferred); - var totalUploaded = currentUploads.Sum(c => c.Transferred); - var totalToUpload = currentUploads.Sum(c => c.Total); - - UiSharedService.DrawOutlinedFont($"▲", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); - ImGui.SameLine(); - var xDistance = ImGui.GetCursorPosX(); - UiSharedService.DrawOutlinedFont($"Compressing+Uploading {doneUploads}/{totalUploads}", - ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); - ImGui.NewLine(); - ImGui.SameLine(xDistance); - UiSharedService.DrawOutlinedFont( - $"{UiSharedService.ByteToString(totalUploaded, addSuffix: false)}/{UiSharedService.ByteToString(totalToUpload)}", - ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); - - if (_currentDownloads.Any()) ImGui.Separator(); - } - } - catch - { - _logger.LogDebug("Error drawing upload progress"); - } - - try - { - // Check if download notifications are enabled (not set to TextOverlay) - var useNotifications = _configService.Current.UseLightlessNotifications - ? _configService.Current.LightlessDownloadNotification != NotificationLocation.TextOverlay - : _configService.Current.UseNotificationsForDownloads; - - if (useNotifications) - { - // Use notification system - if (_currentDownloads.Any()) - { - UpdateDownloadNotificationIfChanged(limiterSnapshot); - _notificationDismissed = false; - } - else if (!_notificationDismissed) - { - Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); - _notificationDismissed = true; - _lastDownloadStateHash = 0; - } - } - else - { - // Use text overlay - if (limiterSnapshot.IsEnabled) - { - var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey; - var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}"; - queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)"; - UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1); - ImGui.NewLine(); - } - else - { - UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1); - ImGui.NewLine(); - } - - foreach (var item in _currentDownloads.ToList()) - { - var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot); - var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue); - var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading); - var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing); - var totalFiles = item.Value.Sum(c => c.Value.TotalFiles); - var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles); - var totalBytes = item.Value.Sum(c => c.Value.TotalBytes); - var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes); - - UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); - ImGui.SameLine(); - var xDistance = ImGui.GetCursorPosX(); - UiSharedService.DrawOutlinedFont( - $"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]", - ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); - ImGui.NewLine(); - ImGui.SameLine(xDistance); - UiSharedService.DrawOutlinedFont( - $"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})", - ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); - } - } - } - catch - { - _logger.LogDebug("Error drawing download progress"); - } - } - - if (_configService.Current.ShowTransferBars) - { - const int transparency = 100; - const int dlBarBorder = 3; - - foreach (var transfer in _currentDownloads.ToList()) - { - var screenPos = _dalamudUtilService.WorldToScreen(transfer.Key.GetGameObject()); - if (screenPos == Vector2.Zero) continue; - - var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes); - var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes); - - var maxDlText = $"{UiSharedService.ByteToString(totalBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}"; - var textSize = _configService.Current.TransferBarsShowText ? ImGui.CalcTextSize(maxDlText) : new Vector2(10, 10); - - int dlBarHeight = _configService.Current.TransferBarsHeight > ((int)textSize.Y + 5) ? _configService.Current.TransferBarsHeight : (int)textSize.Y + 5; - int dlBarWidth = _configService.Current.TransferBarsWidth > ((int)textSize.X + 10) ? _configService.Current.TransferBarsWidth : (int)textSize.X + 10; - - var dlBarStart = new Vector2(screenPos.X - dlBarWidth / 2f, screenPos.Y - dlBarHeight / 2f); - var dlBarEnd = new Vector2(screenPos.X + dlBarWidth / 2f, screenPos.Y + dlBarHeight / 2f); - var drawList = ImGui.GetBackgroundDrawList(); - drawList.AddRectFilled( - dlBarStart with { X = dlBarStart.X - dlBarBorder - 1, Y = dlBarStart.Y - dlBarBorder - 1 }, - dlBarEnd with { X = dlBarEnd.X + dlBarBorder + 1, Y = dlBarEnd.Y + dlBarBorder + 1 }, - UiSharedService.Color(0, 0, 0, transparency), 1); - drawList.AddRectFilled(dlBarStart with { X = dlBarStart.X - dlBarBorder, Y = dlBarStart.Y - dlBarBorder }, - dlBarEnd with { X = dlBarEnd.X + dlBarBorder, Y = dlBarEnd.Y + dlBarBorder }, - UiSharedService.Color(220, 220, 220, transparency), 1); - drawList.AddRectFilled(dlBarStart, dlBarEnd, - UiSharedService.Color(0, 0, 0, transparency), 1); - var dlProgressPercent = transferredBytes / (double)totalBytes; - drawList.AddRectFilled(dlBarStart, - dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) }, - UiSharedService.Color(UIColors.Get("LightlessPurple"))); - - if (_configService.Current.TransferBarsShowText) - { - var downloadText = $"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}"; - UiSharedService.DrawOutlinedFont(drawList, downloadText, - screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 }, - UiSharedService.Color(255, 255, 255, transparency), - UiSharedService.Color(0, 0, 0, transparency), 1); + UpdateDownloadNotificationIfChanged(limiterSnapshot); + _notificationDismissed = false; + } + else if (!_notificationDismissed) + { + Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); + _notificationDismissed = true; + _lastDownloadStateHash = 0; } } + + DrawDownloadSummaryBox(); if (_configService.Current.ShowUploading) { + const int transparency = 100; foreach (var player in _uploadingPlayers.Select(p => p.Key).ToList()) { var screenPos = _dalamudUtilService.WorldToScreen(player.GetGameObject()); @@ -253,29 +135,621 @@ public class DownloadUi : WindowMediatorSubscriberBase { using var _ = _uiShared.UidFont.Push(); var uploadText = "Uploading"; - var textSize = ImGui.CalcTextSize(uploadText); var drawList = ImGui.GetBackgroundDrawList(); - UiSharedService.DrawOutlinedFont(drawList, uploadText, - screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 }, + UiSharedService.DrawOutlinedFont(drawList, uploadText, screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 }, UiSharedService.Color(255, 255, 0, transparency), - UiSharedService.Color(0, 0, 0, transparency), 2); + UiSharedService.Color(0, 0, 0, transparency), + 2 + ); } catch { - _logger.LogDebug("Error drawing upload progress"); + _logger.LogDebug("Error drawing upload progress"); } } } } + + if (_configService.Current.ShowTransferBars) + { + DrawTransferBar(); + } + } + + private void DrawTransferBar() + { + const int dlBarBorder = 3; + const float rounding = 6f; + var shadowOffset = new Vector2(2, 2); + + foreach (var transfer in _currentDownloads.ToList()) + { + var transferKey = transfer.Key; + var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject()); + + // If RawPos is zero, remove it from smoothed dictionary + if (rawPos == Vector2.Zero) + { + _smoothed.Remove(transferKey); + continue; + } + + // Smoothing out the movement and fix jitter around the position. + Vector2 screenPos = _smoothed.TryGetValue(transferKey, out var lastPos) + ? (rawPos - lastPos).Length() < 4f ? lastPos : rawPos + : rawPos; + _smoothed[transferKey] = screenPos; + + var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes); + var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes); + + // Per-player state counts + var dlSlot = 0; + var dlQueue = 0; + var dlProg = 0; + var dlDecomp = 0; + + foreach (var entry in transfer.Value) + { + var fileStatus = entry.Value; + switch (fileStatus.DownloadStatus) + { + case DownloadStatus.WaitingForSlot: + dlSlot++; + break; + case DownloadStatus.WaitingForQueue: + dlQueue++; + break; + case DownloadStatus.Downloading: + dlProg++; + break; + case DownloadStatus.Decompressing: + dlDecomp++; + break; + } + } + + string statusText; + if (dlProg > 0) + { + statusText = "Downloading"; + } + else if (dlDecomp > 0 || (totalBytes > 0 && transferredBytes >= totalBytes)) + { + statusText = "Decompressing"; + } + else if (dlQueue > 0) + { + statusText = "Waiting for queue"; + } + else if (dlSlot > 0) + { + statusText = "Waiting for slot"; + } + else + { + statusText = "Waiting"; + } + + var hasValidSize = totalBytes > 0; + + var maxDlText = $"{UiSharedService.ByteToString(totalBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}"; + var textSize = _configService.Current.TransferBarsShowText + ? ImGui.CalcTextSize(maxDlText) + : new Vector2(10, 10); + + int dlBarHeight = _configService.Current.TransferBarsHeight > ((int)textSize.Y + 5) + ? _configService.Current.TransferBarsHeight + : (int)textSize.Y + 5; + int dlBarWidth = _configService.Current.TransferBarsWidth > ((int)textSize.X + 10) + ? _configService.Current.TransferBarsWidth + : (int)textSize.X + 10; + + var dlBarStart = new Vector2(screenPos.X - dlBarWidth / 2f, screenPos.Y - dlBarHeight / 2f); + var dlBarEnd = new Vector2(screenPos.X + dlBarWidth / 2f, screenPos.Y + dlBarHeight / 2f); + + // Precompute rects + var outerStart = new Vector2(dlBarStart.X - dlBarBorder - 1, dlBarStart.Y - dlBarBorder - 1); + var outerEnd = new Vector2(dlBarEnd.X + dlBarBorder + 1, dlBarEnd.Y + dlBarBorder + 1); + var borderStart = new Vector2(dlBarStart.X - dlBarBorder, dlBarStart.Y - dlBarBorder); + var borderEnd = new Vector2(dlBarEnd.X + dlBarBorder, dlBarEnd.Y + dlBarBorder); + + var drawList = ImGui.GetBackgroundDrawList(); + + drawList.AddRectFilled( + outerStart + shadowOffset, + outerEnd + shadowOffset, + UiSharedService.Color(0, 0, 0, 100 / 2), + rounding + 2 + ); + drawList.AddRectFilled( + outerStart, + outerEnd, + UiSharedService.Color(0, 0, 0, 100), + rounding + 2 + ); + drawList.AddRectFilled( + borderStart, + borderEnd, + UiSharedService.Color(ImGuiColors.DalamudGrey), + rounding + ); + drawList.AddRectFilled( + dlBarStart, + dlBarEnd, + UiSharedService.Color(0, 0, 0, 100), + rounding + ); + + bool showFill = false; + double fillPercent = 0.0; + + if (hasValidSize) + { + if (dlProg > 0) + { + fillPercent = transferredBytes / (double)totalBytes; + showFill = true; + } + else if (dlDecomp > 0 || transferredBytes >= totalBytes) + { + fillPercent = 1.0; + showFill = true; + } + } + + if (showFill) + { + if (fillPercent < 0) fillPercent = 0; + if (fillPercent > 1) fillPercent = 1; + + var progressEndX = dlBarStart.X + (float)(fillPercent * dlBarWidth); + var progressEnd = new Vector2(progressEndX, dlBarEnd.Y); + + drawList.AddRectFilled( + dlBarStart, + progressEnd, + UiSharedService.Color(UIColors.Get("LightlessPurple")), + rounding + ); + } + + if (_configService.Current.TransferBarsShowText) + { + string downloadText; + + if (dlProg > 0 && hasValidSize) + { + downloadText = + $"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}"; + } + else if ((dlDecomp > 0 || transferredBytes >= totalBytes) && hasValidSize) + { + downloadText = "Decompressing"; + } + else + { + // Waiting states + downloadText = statusText; + } + + var actualTextSize = ImGui.CalcTextSize(downloadText); + + UiSharedService.DrawOutlinedFont( + drawList, + downloadText, + screenPos with + { + X = screenPos.X - actualTextSize.X / 2f - 1, + Y = screenPos.Y - actualTextSize.Y / 2f - 1 + }, + UiSharedService.Color(ImGuiColors.DalamudGrey), + UiSharedService.Color(0, 0, 0, 100), + 1 + ); + } + } + + if (_configService.Current.ShowUploading) + { + foreach (var player in _uploadingPlayers.Select(p => p.Key).ToList()) + { + var screenPos = _dalamudUtilService.WorldToScreen(player.GetGameObject()); + if (screenPos == Vector2.Zero) continue; + + try + { + using var _ = _uiShared.UidFont.Push(); + var uploadText = "Uploading"; + + var textSize = ImGui.CalcTextSize(uploadText); + + var drawList = ImGui.GetBackgroundDrawList(); + UiSharedService.DrawOutlinedFont(drawList, uploadText, screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 }, + UiSharedService.Color(ImGuiColors.DalamudYellow), + UiSharedService.Color(0, 0, 0, 100), + 2 + ); + } + catch + { + _logger.LogDebug("Error drawing upload progress"); + } + } + } + } + + private void DrawDownloadSummaryBox() + { + if (_currentDownloads.IsEmpty) + return; + + const float padding = 6f; + const float spacingY = 2f; + const float minBoxWidth = 320f; + + var now = ImGui.GetTime(); + + int totalFiles = 0; + int transferredFiles = 0; + long totalBytes = 0; + long transferredBytes = 0; + + var totalDlSlot = 0; + var totalDlQueue = 0; + var totalDlProg = 0; + var totalDlDecomp = 0; + + var perPlayer = new List<( + string Name, + int TransferredFiles, + int TotalFiles, + long TransferredBytes, + long TotalBytes, + double SpeedBytesPerSecond, + int DlSlot, + int DlQueue, + int DlProg, + int DlDecomp)>(); + + foreach (var transfer in _currentDownloads) + { + var handler = transfer.Key; + var statuses = transfer.Value.Values; + + var playerTotalFiles = statuses.Sum(s => s.TotalFiles); + var playerTransferredFiles = statuses.Sum(s => s.TransferredFiles); + var playerTotalBytes = statuses.Sum(s => s.TotalBytes); + var playerTransferredBytes = statuses.Sum(s => s.TransferredBytes); + + totalFiles += playerTotalFiles; + transferredFiles += playerTransferredFiles; + totalBytes += playerTotalBytes; + transferredBytes += playerTransferredBytes; + + // per-player W/Q/P/D + var playerDlSlot = 0; + var playerDlQueue = 0; + var playerDlProg = 0; + var playerDlDecomp = 0; + + foreach (var entry in transfer.Value) + { + var fileStatus = entry.Value; + switch (fileStatus.DownloadStatus) + { + case DownloadStatus.WaitingForSlot: + playerDlSlot++; + totalDlSlot++; + break; + case DownloadStatus.WaitingForQueue: + playerDlQueue++; + totalDlQueue++; + break; + case DownloadStatus.Downloading: + playerDlProg++; + totalDlProg++; + break; + case DownloadStatus.Decompressing: + playerDlDecomp++; + totalDlDecomp++; + break; + } + } + + double speed = 0; + if (playerTotalBytes > 0) + { + if (!_downloadSpeeds.TryGetValue(handler, out var tracker)) + { + tracker = new DownloadSpeedTracker(windowSeconds: 3.0); + _downloadSpeeds[handler] = tracker; + } + + speed = tracker.Update(now, playerTransferredBytes); + } + + perPlayer.Add(( + handler.Name, + playerTransferredFiles, + playerTotalFiles, + playerTransferredBytes, + playerTotalBytes, + speed, + playerDlSlot, + playerDlQueue, + playerDlProg, + playerDlDecomp + )); + } + + // Clean speed trackers for players with no active downloads + foreach (var handler in _downloadSpeeds.Keys.ToList()) + { + if (!_currentDownloads.ContainsKey(handler)) + _downloadSpeeds.Remove(handler); + } + + if (totalFiles == 0 || totalBytes == 0) + return; + + // max speed for per-player bar scale (clamped) + double maxSpeed = perPlayer.Count > 0 ? perPlayer.Max(p => p.SpeedBytesPerSecond) : 0; + if (maxSpeed <= 0) + maxSpeed = 1; + + var drawList = ImGui.GetBackgroundDrawList(); + var windowPos = ImGui.GetWindowPos(); + + // Overall texts + var headerText = + $"Downloading {transferredFiles}/{totalFiles} files [W:{totalDlSlot}/Q:{totalDlQueue}/P:{totalDlProg}/D:{totalDlDecomp}]"; + + var bytesText = + $"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}"; + + var totalSpeed = perPlayer.Sum(p => p.SpeedBytesPerSecond); + var speedText = totalSpeed > 0 + ? $"{UiSharedService.ByteToString((long)totalSpeed)}/s" + : "Calculating in lightspeed..."; + + var headerSize = ImGui.CalcTextSize(headerText); + var bytesSize = ImGui.CalcTextSize(bytesText); + var totalSpeedSize = ImGui.CalcTextSize(speedText); + + float contentWidth = headerSize.X; + if (bytesSize.X > contentWidth) contentWidth = bytesSize.X; + if (totalSpeedSize.X > contentWidth) contentWidth = totalSpeedSize.X; + + if (_configService.Current.ShowPlayerLinesTransferWindow) + { + foreach (var p in perPlayer) + { + var line = + $"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}"; + + var lineSize = ImGui.CalcTextSize(line); + if (lineSize.X > contentWidth) + contentWidth = lineSize.X; + } + + } + + var lineHeight = ImGui.GetTextLineHeight(); + var globalBarHeight = lineHeight * 0.8f; + var perPlayerBarHeight = lineHeight * 0.4f; + + // Box width + float boxWidth = contentWidth + padding * 2; + if (boxWidth < minBoxWidth) + boxWidth = minBoxWidth; + + // Box height + float boxHeight = 0; + boxHeight += padding; + boxHeight += globalBarHeight; + boxHeight += padding; + + boxHeight += lineHeight + spacingY; + boxHeight += lineHeight + spacingY; + boxHeight += lineHeight * 1.4f + spacingY; + + if (_configService.Current.ShowPlayerLinesTransferWindow) + { + foreach (var p in perPlayer) + { + boxHeight += lineHeight + spacingY; + + var showBar = _configService.Current.ShowPlayerSpeedBarsTransferWindow + && p.TransferredBytes > 0; + + if (showBar) + { + boxHeight += perPlayerBarHeight + spacingY; + } + } + } + + boxHeight += padding; + + var boxMin = windowPos; + var boxMax = new Vector2(windowPos.X + boxWidth, windowPos.Y + boxHeight); + + // Background + border + drawList.AddRectFilled(boxMin, boxMax, UiSharedService.Color(0, 0, 0, _transferBoxTransparency), 5f); + drawList.AddRect(boxMin, boxMax, UiSharedService.Color(ImGuiColors.DalamudGrey), 5f); + + var cursor = boxMin + new Vector2(padding, padding); + + var barMin = cursor; + var barMax = new Vector2(boxMin.X + boxWidth - padding, cursor.Y + globalBarHeight); + + var progress = (float)transferredBytes / totalBytes; + if (progress < 0f) progress = 0f; + if (progress > 1f) progress = 1f; + + drawList.AddRectFilled(barMin, barMax, UiSharedService.Color(40, 40, 40, _transferBoxTransparency), 3f); + drawList.AddRectFilled( + barMin, + new Vector2(barMin.X + (barMax.X - barMin.X) * progress, barMax.Y), + UiSharedService.Color(UIColors.Get("LightlessPurple")), + 3f + ); + + cursor.Y = barMax.Y + padding; + + // Header + UiSharedService.DrawOutlinedFont( + drawList, + headerText, + cursor, + UiSharedService.Color(ImGuiColors.DalamudWhite), + UiSharedService.Color(0, 0, 0, _transferBoxTransparency), + 1 + ); + cursor.Y += lineHeight + spacingY; + + // Bytes + UiSharedService.DrawOutlinedFont( + drawList, + bytesText, + cursor, + UiSharedService.Color(ImGuiColors.DalamudWhite), + UiSharedService.Color(0, 0, 0, _transferBoxTransparency), + 1 + ); + cursor.Y += lineHeight + spacingY; + + // Total speed + UiSharedService.DrawOutlinedFont( + drawList, + speedText, + cursor, + UiSharedService.Color(UIColors.Get("LightlessPurple")), + UiSharedService.Color(0, 0, 0, _transferBoxTransparency), + 1 + ); + cursor.Y += lineHeight * 1.4f + spacingY; + + var orderedPlayers = perPlayer.OrderByDescending(p => p.TotalBytes).ToList(); + + foreach (var p in orderedPlayers) + { + var hasSpeed = p.SpeedBytesPerSecond > 0; + var playerSpeedText = hasSpeed + ? $"{UiSharedService.ByteToString((long)p.SpeedBytesPerSecond)}/s" + : "-"; + + var showBar = _configService.Current.ShowPlayerSpeedBarsTransferWindow + && p.TransferredBytes > 0; + + var labelLine = + $"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}"; + + if (!showBar) + { + UiSharedService.DrawOutlinedFont( + drawList, + labelLine, + cursor, + UiSharedService.Color(255, 255, 255, _transferBoxTransparency), + UiSharedService.Color(0, 0, 0, _transferBoxTransparency), + 1 + ); + + cursor.Y += lineHeight + spacingY; + continue; + } + + UiSharedService.DrawOutlinedFont( + drawList, + labelLine, + cursor, + UiSharedService.Color(255, 255, 255, _transferBoxTransparency), + UiSharedService.Color(0, 0, 0, _transferBoxTransparency), + 1 + ); + cursor.Y += lineHeight + spacingY; + + // Bar background + var barBgMin = new Vector2(boxMin.X + padding, cursor.Y); + var barBgMax = new Vector2(boxMax.X - padding, cursor.Y + perPlayerBarHeight); + + drawList.AddRectFilled( + barBgMin, + barBgMax, + UiSharedService.Color(40, 40, 40, _transferBoxTransparency), + 3f + ); + + // Fill based on Progress of download + float ratio = 0f; + if (p.TotalBytes > 0) + ratio = (float)p.TransferredBytes / p.TotalBytes; + + if (ratio < 0f) ratio = 0f; + if (ratio > 1f) ratio = 1f; + + var fillX = barBgMin.X + (barBgMax.X - barBgMin.X) * ratio; + var barFillMax = new Vector2(fillX, barBgMax.Y); + + drawList.AddRectFilled( + barBgMin, + barFillMax, + UiSharedService.Color(UIColors.Get("LightlessPurple")), + 3f + ); + + // Text inside bar: downloading vs decompressing + string barText; + + var isDecompressing = p.DlDecomp > 0 && p.TransferredBytes >= p.TotalBytes && p.TotalBytes > 0; + + if (isDecompressing) + { + // Keep bar full, static text showing decompressing + barText = "Decompressing..."; + } + else + { + var bytesInside = + $"{UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)}"; + + barText = hasSpeed + ? $"{bytesInside} @ {playerSpeedText}" + : bytesInside; + } + + if (!string.IsNullOrEmpty(barText)) + { + var barTextSize = ImGui.CalcTextSize(barText); + + var barTextPos = new Vector2( + barBgMin.X + ((barBgMax.X - barBgMin.X) - barTextSize.X) / 2f - 1, + barBgMin.Y + ((perPlayerBarHeight - barTextSize.Y) / 2f) - 1 + ); + + UiSharedService.DrawOutlinedFont( + drawList, + barText, + barTextPos, + UiSharedService.Color(255, 255, 255, _transferBoxTransparency), + UiSharedService.Color(0, 0, 0, _transferBoxTransparency), + 1 + ); + } + + cursor.Y += perPlayerBarHeight + spacingY; + } } public override bool DrawConditions() { if (_uiShared.EditTrackerPosition) return true; if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false; - if (!_currentDownloads.Any() && !_fileTransferManager.IsUploading && !_uploadingPlayers.Any()) return false; + if (_currentDownloads.IsEmpty && !_fileTransferManager.IsUploading && _uploadingPlayers.IsEmpty) return false; if (!IsOpen) return false; return true; } @@ -369,4 +843,70 @@ public class DownloadUi : WindowMediatorSubscriberBase } } } + + private sealed class DownloadSpeedTracker + { + private readonly Queue<(double Time, long Bytes)> _samples = new(); + private readonly double _windowSeconds; + + public double SpeedBytesPerSecond { get; private set; } + + public DownloadSpeedTracker(double windowSeconds = 3.0) + { + _windowSeconds = windowSeconds; + } + + public double Update(double now, long totalBytes) + { + if (_samples.Count > 0 && totalBytes < _samples.Last().Bytes) + { + _samples.Clear(); + } + + _samples.Enqueue((now, totalBytes)); + + while (_samples.Count > 0 && now - _samples.Peek().Time > _windowSeconds) + _samples.Dequeue(); + + if (_samples.Count < 2) + { + SpeedBytesPerSecond = 0; + return SpeedBytesPerSecond; + } + + var oldest = _samples.Peek(); + var newest = _samples.Last(); + + var dt = newest.Time - oldest.Time; + if (dt <= 0.0001) + { + SpeedBytesPerSecond = 0; + return SpeedBytesPerSecond; + } + + var dBytes = newest.Bytes - oldest.Bytes; + if (dBytes <= 0) + { + SpeedBytesPerSecond = 0; + return SpeedBytesPerSecond; + } + + + const long minBytesForSpeed = 32 * 1024; + if (dBytes < minBytesForSpeed) + { + + return SpeedBytesPerSecond; + } + + var avg = dBytes / dt; + + const double alpha = 0.3; + SpeedBytesPerSecond = SpeedBytesPerSecond <= 0 + ? avg + : SpeedBytesPerSecond * (1 - alpha) + avg * alpha; + + return SpeedBytesPerSecond; + } + } } \ No newline at end of file diff --git a/LightlessSync/UI/DrawEntityFactory.cs b/LightlessSync/UI/DrawEntityFactory.cs index d1410ad..3c71f5c 100644 --- a/LightlessSync/UI/DrawEntityFactory.cs +++ b/LightlessSync/UI/DrawEntityFactory.cs @@ -1,14 +1,21 @@ -using LightlessSync.API.Dto.Group; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; +using LightlessSync.PlayerData.Factories; +using LightlessSync.PlayerData.Pairs; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Components; using LightlessSync.UI.Handlers; +using LightlessSync.UI.Models; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; -using System.Collections.Immutable; namespace LightlessSync.UI; @@ -19,6 +26,7 @@ public class DrawEntityFactory private readonly LightlessMediator _mediator; private readonly SelectPairForTagUi _selectPairForTagUi; private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly LightlessConfigService _configService; private readonly UiSharedService _uiSharedService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly CharaDataManager _charaDataManager; @@ -29,13 +37,28 @@ public class DrawEntityFactory private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi; private readonly TagHandler _tagHandler; private readonly IdDisplayHandler _uidDisplayHandler; + private readonly PairLedger _pairLedger; + private readonly PairFactory _pairFactory; - public DrawEntityFactory(ILogger logger, ApiController apiController, IdDisplayHandler uidDisplayHandler, - SelectTagForPairUi selectTagForPairUi, RenamePairTagUi renamePairTagUi, LightlessMediator mediator, - TagHandler tagHandler, SelectPairForTagUi selectPairForTagUi, - ServerConfigurationManager serverConfigurationManager, UiSharedService uiSharedService, - PlayerPerformanceConfigService playerPerformanceConfigService, CharaDataManager charaDataManager, - SelectTagForSyncshellUi selectTagForSyncshellUi, RenameSyncshellTagUi renameSyncshellTagUi, SelectSyncshellForTagUi selectSyncshellForTagUi) + public DrawEntityFactory( + ILogger logger, + ApiController apiController, + IdDisplayHandler uidDisplayHandler, + SelectTagForPairUi selectTagForPairUi, + RenamePairTagUi renamePairTagUi, + LightlessMediator mediator, + TagHandler tagHandler, + SelectPairForTagUi selectPairForTagUi, + ServerConfigurationManager serverConfigurationManager, + LightlessConfigService configService, + UiSharedService uiSharedService, + PlayerPerformanceConfigService playerPerformanceConfigService, + CharaDataManager charaDataManager, + SelectTagForSyncshellUi selectTagForSyncshellUi, + RenameSyncshellTagUi renameSyncshellTagUi, + SelectSyncshellForTagUi selectSyncshellForTagUi, + PairLedger pairLedger, + PairFactory pairFactory) { _logger = logger; _apiController = apiController; @@ -46,44 +69,152 @@ public class DrawEntityFactory _tagHandler = tagHandler; _selectPairForTagUi = selectPairForTagUi; _serverConfigurationManager = serverConfigurationManager; + _configService = configService; _uiSharedService = uiSharedService; _playerPerformanceConfigService = playerPerformanceConfigService; _charaDataManager = charaDataManager; _selectTagForSyncshellUi = selectTagForSyncshellUi; _renameSyncshellTagUi = renameSyncshellTagUi; _selectSyncshellForTagUi = selectSyncshellForTagUi; + _pairLedger = pairLedger; + _pairFactory = pairFactory; } - public DrawFolderGroup CreateDrawGroupFolder(GroupFullInfoDto groupFullInfoDto, - Dictionary> filteredPairs, - IImmutableList allPairs) + public DrawFolderGroup CreateGroupFolder( + string id, + GroupFullInfoDto groupFullInfo, + IEnumerable drawEntries, + IEnumerable allEntries) { - return new DrawFolderGroup(groupFullInfoDto.Group.GID, groupFullInfoDto, _apiController, - filteredPairs.Select(p => CreateDrawPair(groupFullInfoDto.Group.GID + p.Key.UserData.UID, p.Key, p.Value, groupFullInfoDto)).ToImmutableList(), - allPairs, _tagHandler, _uidDisplayHandler, _mediator, _uiSharedService, _selectTagForSyncshellUi); + var drawPairs = drawEntries + .Select(entry => CreateDrawPair($"{id}:{entry.DisplayEntry.Ident.UserId}", entry, groupFullInfo)) + .Where(draw => draw is not null) + .Cast() + .ToImmutableList(); + + var allPairs = allEntries.ToImmutableList(); + + return new DrawFolderGroup( + id, + groupFullInfo, + _apiController, + drawPairs, + allPairs, + _tagHandler, + _uidDisplayHandler, + _mediator, + _uiSharedService, + _selectTagForSyncshellUi); } - public DrawFolderGroup CreateDrawGroupFolder(string id, GroupFullInfoDto groupFullInfoDto, - Dictionary> filteredPairs, - IImmutableList allPairs) + public DrawFolderTag CreateTagFolder( + string tag, + IEnumerable drawEntries, + IEnumerable allEntries) { - return new DrawFolderGroup(id, groupFullInfoDto, _apiController, - filteredPairs.Select(p => CreateDrawPair(groupFullInfoDto.Group.GID + p.Key.UserData.UID, p.Key, p.Value, groupFullInfoDto)).ToImmutableList(), - allPairs, _tagHandler, _uidDisplayHandler, _mediator, _uiSharedService, _selectTagForSyncshellUi); + var drawPairs = drawEntries + .Select(entry => CreateDrawPair($"{tag}:{entry.DisplayEntry.Ident.UserId}", entry)) + .Where(draw => draw is not null) + .Cast() + .ToImmutableList(); + + var allPairs = allEntries.ToImmutableList(); + + return new DrawFolderTag( + tag, + drawPairs, + allPairs, + _tagHandler, + _apiController, + _selectPairForTagUi, + _renamePairTagUi, + _uiSharedService, + _serverConfigurationManager, + _configService, + _mediator); } - public DrawFolderTag CreateDrawTagFolder(string tag, - Dictionary> filteredPairs, - IImmutableList allPairs) + public DrawUserPair? CreateDrawPair( + string id, + PairUiEntry entry, + GroupFullInfoDto? currentGroup = null) { - return new(tag, filteredPairs.Select(u => CreateDrawPair(tag, u.Key, u.Value, currentGroup: null)).ToImmutableList(), - allPairs, _tagHandler, _apiController, _selectPairForTagUi, _renamePairTagUi, _uiSharedService); + var pair = _pairFactory.Create(entry.DisplayEntry); + if (pair is null) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Skipping draw pair for {UserId}: legacy pair not found.", entry.DisplayEntry.Ident.UserId); + } + return null; + } + + return new DrawUserPair( + id, + entry, + pair, + currentGroup, + _apiController, + _uidDisplayHandler, + _mediator, + _selectTagForPairUi, + _serverConfigurationManager, + _uiSharedService, + _playerPerformanceConfigService, + _configService, + _charaDataManager, + _pairLedger); } - public DrawUserPair CreateDrawPair(string id, Pair user, List groups, GroupFullInfoDto? currentGroup) + public IReadOnlyList GetAllEntries() { - return new DrawUserPair(id + user.UserData.UID, user, groups, currentGroup, _apiController, _uidDisplayHandler, - _mediator, _selectTagForPairUi, _serverConfigurationManager, _uiSharedService, _playerPerformanceConfigService, - _charaDataManager); + try + { + return _pairLedger.GetAllEntries() + .Select(BuildUiEntry) + .ToList(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to build pair display entries."); + return Array.Empty(); + } + } + + private PairUiEntry BuildUiEntry(PairDisplayEntry entry) + { + var handler = entry.Handler; + var alias = entry.User.AliasOrUID; + if (string.IsNullOrWhiteSpace(alias)) + { + alias = entry.Ident.UserId; + } + + var displayName = !string.IsNullOrWhiteSpace(handler?.PlayerName) + ? handler!.PlayerName! + : alias; + + var note = _serverConfigurationManager.GetNoteForUid(entry.Ident.UserId) ?? string.Empty; + var isPaused = entry.SelfPermissions.IsPaused(); + + return new PairUiEntry( + entry, + alias, + displayName, + note, + entry.IsVisible, + entry.IsOnline, + entry.IsDirectlyPaired, + entry.IsOneSided, + entry.HasAnyConnection, + isPaused, + entry.SelfPermissions, + entry.OtherPermissions, + entry.PairStatus, + handler?.LastAppliedDataBytes ?? -1, + handler?.LastAppliedDataTris ?? -1, + handler?.LastAppliedApproximateVRAMBytes ?? -1, + handler?.LastAppliedApproximateEffectiveVRAMBytes ?? -1, + handler); } } \ No newline at end of file diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index 17bc871..9cadb4c 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -2,10 +2,8 @@ using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Plugin.Services; -using Dalamud.Utility; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Configurations; -using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; @@ -14,10 +12,11 @@ using LightlessSync.WebAPI; using LightlessSync.WebAPI.SignalR.Utils; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; using System.Runtime.InteropServices; using System.Text; +using LightlessSync.UI.Services; using static LightlessSync.Services.PairRequestService; +using LightlessSync.Services.LightFinder; namespace LightlessSync.UI; @@ -34,10 +33,10 @@ public sealed class DtrEntry : IDisposable, IHostedService private readonly Lazy _statusEntry; private readonly Lazy _lightfinderEntry; private readonly ILogger _logger; - private readonly BroadcastService _broadcastService; - private readonly BroadcastScannerService _broadcastScannerService; + private readonly LightFinderService _broadcastService; + private readonly LightFinderScannerService _broadcastScannerService; private readonly LightlessMediator _lightlessMediator; - private readonly PairManager _pairManager; + private readonly PairUiService _pairUiService; private readonly PairRequestService _pairRequestService; private readonly DalamudUtilService _dalamudUtilService; private Task? _runTask; @@ -57,12 +56,12 @@ public sealed class DtrEntry : IDisposable, IHostedService IDtrBar dtrBar, ConfigurationServiceBase configService, LightlessMediator lightlessMediator, - PairManager pairManager, + PairUiService pairUiService, PairRequestService pairRequestService, ApiController apiController, ServerConfigurationManager serverManager, - BroadcastService broadcastService, - BroadcastScannerService broadcastScannerService, + LightFinderService broadcastService, + LightFinderScannerService broadcastScannerService, DalamudUtilService dalamudUtilService) { _logger = logger; @@ -71,7 +70,7 @@ public sealed class DtrEntry : IDisposable, IHostedService _lightfinderEntry = new(CreateLightfinderEntry); _configService = configService; _lightlessMediator = lightlessMediator; - _pairManager = pairManager; + _pairUiService = pairUiService; _pairRequestService = pairRequestService; _apiController = apiController; _serverManager = serverManager; @@ -165,7 +164,7 @@ public sealed class DtrEntry : IDisposable, IHostedService entry.OnClick = interactionEvent => OnLightfinderEntryClick(interactionEvent); return entry; } - + private void OnStatusEntryClick(DtrInteractionEvent interactionEvent) { if (interactionEvent.ClickType.Equals(MouseClickType.Left)) @@ -254,16 +253,15 @@ public sealed class DtrEntry : IDisposable, IHostedService if (_apiController.IsConnected) { - var pairCount = _pairManager.GetVisibleUserCount(); + var snapshot = _pairUiService.GetSnapshot(); + var visiblePairsQuery = snapshot.PairsByUid.Values.Where(x => x.IsVisible && !x.IsPaused); + var pairCount = visiblePairsQuery.Count(); text = $"\uE044 {pairCount}"; if (pairCount > 0) { var preferNote = config.PreferNoteInDtrTooltip; var showUid = config.ShowUidInDtrTooltip; - var visiblePairsQuery = _pairManager.GetOnlineUserPairs() - .Where(x => x.IsVisible); - IEnumerable visiblePairs = showUid ? visiblePairsQuery.Select(x => string.Format("{0} ({1})", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID)) : visiblePairsQuery.Select(x => string.Format("{0}", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName)); @@ -445,7 +443,7 @@ public sealed class DtrEntry : IDisposable, IHostedService return ($"{icon} OFF", colors, tooltip.ToString()); } - private (string, Colors, string) FormatTooltip(string title, IEnumerable names, string icon, Colors color) + private static (string, Colors, string) FormatTooltip(string title, IEnumerable names, string icon, Colors color) { var list = names.Where(x => !string.IsNullOrEmpty(x)).ToList(); var tooltip = new StringBuilder() @@ -490,7 +488,7 @@ public sealed class DtrEntry : IDisposable, IHostedService private const byte _colorTypeForeground = 0x13; private const byte _colorTypeGlow = 0x14; - private static Colors SwapColorChannels(Colors colors) + internal static Colors SwapColorChannels(Colors colors) => new(SwapColorComponent(colors.Foreground), SwapColorComponent(colors.Glow)); private static uint SwapColorComponent(uint color) diff --git a/LightlessSync/UI/EditProfileUi.Group.cs b/LightlessSync/UI/EditProfileUi.Group.cs new file mode 100644 index 0000000..7b47ced --- /dev/null +++ b/LightlessSync/UI/EditProfileUi.Group.cs @@ -0,0 +1,691 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using LightlessSync.API.Dto.Group; +using LightlessSync.Services.Mediator; +using LightlessSync.Services.Profiles; +using LightlessSync.UI.Tags; +using LightlessSync.Utils; +using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using System.Numerics; + +namespace LightlessSync.UI; + +public partial class EditProfileUi +{ + private void OpenGroupEditor(GroupFullInfoDto groupInfo) + { + _mode = ProfileEditorMode.Group; + _groupInfo = groupInfo; + + var profile = _lightlessProfileManager.GetLightlessGroupProfile(groupInfo.Group); + _groupProfileData = profile; + SyncGroupProfileState(profile, resetSelection: true); + + var scale = ImGuiHelpers.GlobalScale; + var viewport = ImGui.GetMainViewport(); + ProfileEditorLayoutCoordinator.Enable(groupInfo.Group.GID); + ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale); + Mediator.Publish(new GroupProfileOpenStandaloneMessage(groupInfo.Group)); + + IsOpen = true; + _wasOpen = true; + } + + private void ResetGroupEditorState() + { + _groupInfo = null; + _groupProfileData = null; + _groupIsNsfw = false; + _groupIsDisabled = false; + _groupServerIsNsfw = false; + _groupServerIsDisabled = false; + _queuedProfileImage = null; + _queuedBannerImage = null; + _profileImage = Array.Empty(); + _bannerImage = Array.Empty(); + _profileDescription = string.Empty; + _descriptionText = string.Empty; + _profileTagIds = Array.Empty(); + _tagEditorSelection.Clear(); + _pfpTextureWrap?.Dispose(); + _pfpTextureWrap = null; + _bannerTextureWrap?.Dispose(); + _bannerTextureWrap = null; + _showProfileImageError = false; + _showBannerImageError = false; + _groupVisibilityInitialized = false; + } + + private void DrawGroupEditor(float scale) + { + if (_groupInfo is null) + { + UiSharedService.TextWrapped("Open the Syncshell admin panel and choose a group to edit its profile."); + return; + } + + var viewport = ImGui.GetMainViewport(); + var linked = ProfileEditorLayoutCoordinator.IsActive(_groupInfo.Group.GID); + + if (linked) + { + ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale); + + var desiredSize = ProfileEditorLayoutCoordinator.GetEditorSize(scale); + if (!ProfileEditorLayoutCoordinator.NearlyEquals(ImGui.GetWindowSize(), desiredSize)) + ImGui.SetWindowSize(desiredSize, ImGuiCond.Always); + + var currentPos = ImGui.GetWindowPos(); + if (IsWindowBeingDragged()) + ProfileEditorLayoutCoordinator.UpdateAnchorFromEditor(currentPos, scale); + + var desiredPos = ProfileEditorLayoutCoordinator.GetEditorPosition(scale); + if (!ProfileEditorLayoutCoordinator.NearlyEquals(currentPos, desiredPos)) + ImGui.SetWindowPos(desiredPos, ImGuiCond.Always); + } + else + { + var defaultProfilePos = viewport.WorkPos + new Vector2(50f, 70f) * scale; + var defaultEditorPos = defaultProfilePos + ProfileEditorLayoutCoordinator.GetEditorOffset(scale); + ImGui.SetWindowPos(defaultEditorPos, ImGuiCond.FirstUseEver); + } + + if (_queuedProfileImage is not null) + ApplyQueuedGroupProfileImage(); + if (_queuedBannerImage is not null) + ApplyQueuedGroupBannerImage(); + + var profile = _lightlessProfileManager.GetLightlessGroupProfile(_groupInfo.Group); + _groupProfileData = profile; + SyncGroupProfileState(profile, resetSelection: false); + + var accent = UIColors.Get("LightlessPurple"); + var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.015f); + var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.07f); + + using var panelBg = ImRaii.PushColor(ImGuiCol.ChildBg, accentBg); + using var panelBorder = ImRaii.PushColor(ImGuiCol.ChildBg, accentBorder); + ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale); + + if (ImGui.BeginChild("##GroupProfileEditorCanvas", -Vector2.One, true)) + { + DrawGroupGuidelinesSection(scale); + ImGui.Dummy(new Vector2(0f, 4f * scale)); + DrawGroupProfileContent(profile, scale); + } + + ImGui.EndChild(); + ImGui.PopStyleVar(); + } + + private void DrawGroupGuidelinesSection(float scale) + { + DrawSection("Guidelines", scale, () => + { + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1f, 1f)); + + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "All users that are paired and unpaused with you will be able to see your profile pictures, tags and description."); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Other users have the possibility to report this profile for breaking the rules."); + _uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.)"); + _uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Slurs of any kind in the description that can be considered highly offensive"); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "In case of valid reports from other users this can lead to disabling the profile forever or terminating syncshell owner's Lightless account indefinitely."); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Judgement of the profile validity from reports through staff is not up to debate and the decisions to disable the profile or your account permanent."); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"), "If the profile picture or profile description could be considered NSFW, enable the toggle in visibility settings."); + + ImGui.PopStyleVar(); + }); + } + + private void DrawGroupProfileContent(LightlessGroupProfileData profile, float scale) + { + DrawSection("Profile Preview", scale, () => DrawGroupProfileSnapshot(profile, scale)); + DrawSection("Profile Image", scale, DrawGroupProfileImageControls); + DrawSection("Profile Banner", scale, DrawGroupProfileBannerControls); + DrawSection("Profile Description", scale, DrawGroupProfileDescriptionEditor); + DrawSection("Profile Tags", scale, () => DrawGroupProfileTagsEditor(scale)); + DrawSection("Visibility", scale, DrawGroupProfileVisibilityControls); + } + + private void DrawGroupProfileSnapshot(LightlessGroupProfileData profile, float scale) + { + var bannerHeight = 140f * scale; + ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale); + if (ImGui.BeginChild("##GroupProfileBannerPreview", new Vector2(-1f, bannerHeight), true)) + { + if (_bannerTextureWrap != null) + { + var childSize = ImGui.GetWindowSize(); + var padding = ImGui.GetStyle().WindowPadding; + var contentSize = new Vector2( + MathF.Max(childSize.X - padding.X * 2f, 1f), + MathF.Max(childSize.Y - padding.Y * 2f, 1f)); + + var imageSize = ImGuiHelpers.ScaledVector2(_bannerTextureWrap.Width, _bannerTextureWrap.Height); + if (imageSize.X > contentSize.X || imageSize.Y > contentSize.Y) + { + var ratio = MathF.Min(contentSize.X / MathF.Max(imageSize.X, 1f), contentSize.Y / MathF.Max(imageSize.Y, 1f)); + imageSize *= ratio; + } + + var offset = new Vector2( + MathF.Max((contentSize.X - imageSize.X) * 0.5f, 0f), + MathF.Max((contentSize.Y - imageSize.Y) * 0.5f, 0f)); + ImGui.SetCursorPos(padding + offset); + ImGui.Image(_bannerTextureWrap.Handle, imageSize); + } + else + { + ImGui.TextColored(UIColors.Get("LightlessPurple"), "No profile banner uploaded."); + } + } + ImGui.EndChild(); + ImGui.PopStyleVar(); + + ImGui.Dummy(new Vector2(0f, 6f * scale)); + + if (_pfpTextureWrap != null) + { + var size = ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height); + var maxEdge = 160f * scale; + if (size.X > maxEdge || size.Y > maxEdge) + { + var ratio = MathF.Min(maxEdge / MathF.Max(size.X, 1f), maxEdge / MathF.Max(size.Y, 1f)); + size *= ratio; + } + + ImGui.Image(_pfpTextureWrap.Handle, size); + } + else + { + ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale); + if (ImGui.BeginChild("##GroupProfileImagePlaceholder", new Vector2(160f * scale, 160f * scale), true)) + ImGui.TextColored(UIColors.Get("LightlessPurple"), "No profile picture uploaded."); + ImGui.EndChild(); + ImGui.PopStyleVar(); + } + + ImGui.SameLine(); + ImGui.BeginGroup(); + ImGui.TextColored(UIColors.Get("LightlessBlue"), _groupInfo!.GroupAliasOrGID); + ImGui.TextDisabled($"ID: {_groupInfo.Group.GID}"); + ImGui.TextDisabled($"Owner: {_groupInfo.Owner.AliasOrUID}"); + ImGui.EndGroup(); + + ImGui.Dummy(new Vector2(0f, 4f * scale)); + ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale); + if (ImGui.BeginChild("##GroupProfileDescriptionPreview", new Vector2(-1f, 120f * scale), true)) + { + var hasDescription = !string.IsNullOrWhiteSpace(profile.Description); + ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X); + if (!hasDescription) + { + ImGui.TextDisabled("Syncshell has no description set."); + } + else if (!SeStringUtils.TryRenderSeStringMarkupAtCursor(profile.Description!)) + { + UiSharedService.TextWrapped(profile.Description); + } + ImGui.PopTextWrapPos(); + } + ImGui.EndChild(); + ImGui.PopStyleVar(); + + } + + private void DrawGroupProfileImageControls() + { + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "Profile pictures must be 512x512 and under 2 MiB."); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) + { + _fileDialogManager.OpenFileDialog("Select syncshell profile picture", _imageFileDialogFilter, (success, file) => + { + if (!success || string.IsNullOrEmpty(file)) + return; + _showProfileImageError = false; + _ = SubmitGroupProfilePicture(file); + }); + } + UiSharedService.AttachToolTip("Select an image up to 512x512 pixels and <= 2 MiB (PNG/JPG/JPEG/WEBP/BMP)."); + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear profile picture")) + { + _ = ClearGroupProfilePicture(); + } + UiSharedService.AttachToolTip("Remove the current profile picture from this syncshell."); + + if (_showProfileImageError) + { + UiSharedService.ColorTextWrapped("Image must be no larger than 512x512 pixels and under 2 MiB.", ImGuiColors.DalamudRed); + } + } + + private void DrawGroupProfileBannerControls() + { + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "Profile banners must be 840x260 and under 2 MiB."); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile banner")) + { + _fileDialogManager.OpenFileDialog("Select syncshell profile banner", _imageFileDialogFilter, (success, file) => + { + if (!success || string.IsNullOrEmpty(file)) + return; + _showBannerImageError = false; + _ = SubmitGroupProfileBanner(file); + }); + } + UiSharedService.AttachToolTip("Select an image up to 840x260 pixels and <= 2 MiB (PNG/JPG/JPEG/WEBP/BMP)."); + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear profile banner")) + { + _ = ClearGroupProfileBanner(); + } + UiSharedService.AttachToolTip("Remove the current profile banner."); + + if (_showBannerImageError) + { + UiSharedService.ColorTextWrapped("Banner must be no larger than 840x260 pixels and under 2 MiB.", ImGuiColors.DalamudRed); + } + } + + private void DrawGroupProfileDescriptionEditor() + { + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(6f, 4f) * ImGuiHelpers.GlobalScale); + var descriptionBoxSize = new Vector2(-1f, 160f * ImGuiHelpers.GlobalScale); + ImGui.InputTextMultiline("##GroupDescription", ref _descriptionText, 1500, descriptionBoxSize); + ImGui.PopStyleVar(); + + ImGui.TextDisabled($"{_descriptionText.Length}/1500 characters"); + ImGui.SameLine(); + ImGuiComponents.HelpMarker(DescriptionMacroTooltip); + + bool changed = !string.Equals(_descriptionText, _profileDescription, StringComparison.Ordinal); + if (!changed) + ImGui.BeginDisabled(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description")) + { + _ = SubmitGroupDescription(_descriptionText); + } + UiSharedService.AttachToolTip("Apply the text above to the syncshell profile description."); + if (!changed) + ImGui.EndDisabled(); + + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description")) + { + _ = SubmitGroupDescription(string.Empty); + } + UiSharedService.AttachToolTip("Remove the profile description."); + } + + private void DrawGroupProfileTagsEditor(float scale) + { + DrawTagEditor( + scale, + contextPrefix: "group", + saveTooltip: "Apply the selected tags to this syncshell profile.", + submitAction: payload => SubmitGroupTagChanges(payload), + allowReorder: true, + sortPayloadBeforeSubmit: false, + onPayloadPrepared: payload => + { + _tagEditorSelection.Clear(); + if (payload.Length > 0) + _tagEditorSelection.AddRange(payload); + }); + } + + private void DrawGroupProfileVisibilityControls() + { + EnsureGroupVisibilityStateInitialised(); + + bool changedNsfw = DrawCheckboxRow("Profile is NSFW", _groupIsNsfw, out var newNsfw, "Flag this profile as not safe for work."); + if (changedNsfw) + _groupIsNsfw = newNsfw; + + bool changedDisabled = DrawCheckboxRow("Disable profile for viewers", _groupIsDisabled, out var newDisabled, "Temporarily hide this profile from members."); + if (changedDisabled) + _groupIsDisabled = newDisabled; + + bool visibilityChanged = (_groupIsNsfw != _groupServerIsNsfw) || (_groupIsDisabled != _groupServerIsDisabled); + + if (!visibilityChanged) + ImGui.BeginDisabled(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Apply Visibility Changes")) + { + _ = SubmitGroupVisibilityChanges(_groupIsNsfw, _groupIsDisabled); + } + UiSharedService.AttachToolTip("Apply the visibility toggles above."); + if (!visibilityChanged) + ImGui.EndDisabled(); + + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.SyncAlt, "Reset")) + { + _groupIsNsfw = _groupServerIsNsfw; + _groupIsDisabled = _groupServerIsDisabled; + } + } + + private string? GetCurrentGroupProfileImageBase64() + { + if (_queuedProfileImage is not null && _queuedProfileImage.Length > 0) + return Convert.ToBase64String(_queuedProfileImage); + + if (!string.IsNullOrWhiteSpace(_groupProfileData?.Base64ProfilePicture)) + return _groupProfileData!.Base64ProfilePicture; + + return _profileImage.Length > 0 ? Convert.ToBase64String(_profileImage) : null; + } + + private string? GetCurrentGroupBannerBase64() + { + if (_queuedBannerImage is not null && _queuedBannerImage.Length > 0) + return Convert.ToBase64String(_queuedBannerImage); + + if (!string.IsNullOrWhiteSpace(_groupProfileData?.Base64BannerPicture)) + return _groupProfileData!.Base64BannerPicture; + + return _bannerImage.Length > 0 ? Convert.ToBase64String(_bannerImage) : null; + } + + private async Task SubmitGroupProfilePicture(string filePath) + { + if (_groupInfo is null) + return; + + try + { + var fileContent = await File.ReadAllBytesAsync(filePath).ConfigureAwait(false); + var stream = new MemoryStream(fileContent); + await using (stream.ConfigureAwait(false)) + { + var format = await Image.DetectFormatAsync(stream).ConfigureAwait(false); + if (!IsSupportedImageFormat(format)) + { + _showProfileImageError = true; + return; + } + + using var image = Image.Load(fileContent); + if (image.Width > 512 || image.Height > 512 || fileContent.Length > 2000 * 1024) + { + _showProfileImageError = true; + return; + } + + await _apiController.GroupSetProfile(new GroupProfileDto( + _groupInfo.Group, + Description: null, + Tags: null, + PictureBase64: Convert.ToBase64String(fileContent), + BannerBase64: null, + IsNsfw: null, + IsDisabled: null)).ConfigureAwait(false); + + _showProfileImageError = false; + _queuedProfileImage = fileContent; + Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group)); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to upload syncshell profile picture."); + } + } + + private async Task ClearGroupProfilePicture() + { + if (_groupInfo is null) + return; + + try + { + await _apiController.GroupSetProfile(new GroupProfileDto( + _groupInfo.Group, + Description: null, + Tags: null, + PictureBase64: null, + BannerBase64: null, + IsNsfw: null, + IsDisabled: null)).ConfigureAwait(false); + + _queuedProfileImage = Array.Empty(); + Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to clear syncshell profile picture."); + } + } + + private async Task SubmitGroupProfileBanner(string filePath) + { + if (_groupInfo is null) + return; + + try + { + var fileContent = await File.ReadAllBytesAsync(filePath).ConfigureAwait(false); + var stream = new MemoryStream(fileContent); + await using (stream.ConfigureAwait(false)) + { + var format = await Image.DetectFormatAsync(stream).ConfigureAwait(false); + if (!IsSupportedImageFormat(format)) + { + _showBannerImageError = true; + return; + } + + using var image = Image.Load(fileContent); + if (image.Width > 840 || image.Height > 260 || fileContent.Length > 2000 * 1024) + { + _showBannerImageError = true; + return; + } + + await _apiController.GroupSetProfile(new GroupProfileDto( + _groupInfo.Group, + Description: null, + Tags: null, + PictureBase64: null, + BannerBase64: Convert.ToBase64String(fileContent), + IsNsfw: null, + IsDisabled: null)).ConfigureAwait(false); + + _showBannerImageError = false; + _queuedBannerImage = fileContent; + Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group)); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to upload syncshell profile banner."); + } + } + + private async Task ClearGroupProfileBanner() + { + if (_groupInfo is null) + return; + + try + { + await _apiController.GroupSetProfile(new GroupProfileDto( + _groupInfo.Group, + Description: null, + Tags: null, + PictureBase64: null, + BannerBase64: null, + IsNsfw: null, + IsDisabled: null)).ConfigureAwait(false); + + _queuedBannerImage = Array.Empty(); + Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to clear syncshell profile banner."); + } + } + + private async Task SubmitGroupDescription(string description) + { + if (_groupInfo is null) + return; + + try + { + await _apiController.GroupSetProfile(new GroupProfileDto( + _groupInfo.Group, + Description: description, + Tags: null, + PictureBase64: GetCurrentGroupProfileImageBase64(), + BannerBase64: GetCurrentGroupBannerBase64(), + IsNsfw: null, + IsDisabled: null)).ConfigureAwait(false); + + _profileDescription = description; + Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to update syncshell profile description."); + } + } + + private void EnsureGroupVisibilityStateInitialised() + { + if (_groupInfo == null || _groupVisibilityInitialized) + return; + + _groupIsNsfw = _groupServerIsNsfw; + _groupIsDisabled = _groupServerIsDisabled; + _groupVisibilityInitialized = true; + } + + private async Task SubmitGroupTagChanges(int[] payload) + { + if (_groupInfo is null) + return; + + try + { + await _apiController.GroupSetProfile(new GroupProfileDto( + _groupInfo.Group, + Description: null, + Tags: payload, + PictureBase64: GetCurrentGroupProfileImageBase64(), + BannerBase64: GetCurrentGroupBannerBase64(), + IsNsfw: null, + IsDisabled: null)).ConfigureAwait(false); + + _profileTagIds = payload.Length == 0 ? [] : [.. payload]; + Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to update syncshell profile tags."); + } + } + + private async Task SubmitGroupVisibilityChanges(bool isNsfw, bool isDisabled) + { + if (_groupInfo is null) + return; + + try + { + await _apiController.GroupSetProfile(new GroupProfileDto( + _groupInfo.Group, + Description: null, + Tags: null, + PictureBase64: GetCurrentGroupProfileImageBase64(), + BannerBase64: GetCurrentGroupBannerBase64(), + IsNsfw: isNsfw, + IsDisabled: isDisabled)).ConfigureAwait(false); + + _groupServerIsNsfw = isNsfw; + _groupServerIsDisabled = isDisabled; + Mediator.Publish(new ClearProfileGroupDataMessage(_groupInfo.Group)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to update syncshell profile visibility."); + } + } + + private void ApplyQueuedGroupProfileImage() + { + if (_queuedProfileImage is null) + return; + + _profileImage = _queuedProfileImage; + _pfpTextureWrap?.Dispose(); + _pfpTextureWrap = _profileImage.Length > 0 ? _uiSharedService.LoadImage(_profileImage) : null; + _queuedProfileImage = null; + } + + private void ApplyQueuedGroupBannerImage() + { + if (_queuedBannerImage is null) + return; + + _bannerImage = _queuedBannerImage; + _bannerTextureWrap?.Dispose(); + _bannerTextureWrap = _bannerImage.Length > 0 ? _uiSharedService.LoadImage(_bannerImage) : null; + _queuedBannerImage = null; + } + + private void SyncGroupProfileState(LightlessGroupProfileData profile, bool resetSelection) + { + if (!_profileImage.SequenceEqual(profile.ProfileImageData.Value)) + { + _profileImage = profile.ProfileImageData.Value; + _pfpTextureWrap?.Dispose(); + _pfpTextureWrap = _profileImage.Length > 0 ? _uiSharedService.LoadImage(_profileImage) : null; + } + + if (!_bannerImage.SequenceEqual(profile.BannerImageData.Value)) + { + _bannerImage = profile.BannerImageData.Value; + _bannerTextureWrap?.Dispose(); + _bannerTextureWrap = _bannerImage.Length > 0 ? _uiSharedService.LoadImage(_bannerImage) : null; + } + + if (!string.Equals(_profileDescription, profile.Description, StringComparison.Ordinal)) + { + _profileDescription = profile.Description; + _descriptionText = _profileDescription; + } + + var tags = profile.Tags ?? Array.Empty(); + if (!TagsEqual(tags, _profileTagIds)) + { + _profileTagIds = tags.Count == 0 ? Array.Empty() : tags.ToArray(); + if (resetSelection) + { + _tagEditorSelection.Clear(); + if (_profileTagIds.Length > 0) + _tagEditorSelection.AddRange(_profileTagIds); + } + } + + _groupServerIsNsfw = profile.IsNsfw; + _groupServerIsDisabled = profile.IsDisabled; + + if (!_groupVisibilityInitialized) + { + _groupIsNsfw = _groupServerIsNsfw; + _groupIsDisabled = _groupServerIsDisabled; + _groupVisibilityInitialized = true; + } + } +} + diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 5dedf81..53d6b9e 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -1,68 +1,134 @@ -using Dalamud.Bindings.ImGui; +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Data; +using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.UI.Style; +using LightlessSync.UI.Tags; using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; using System.Numerics; +using LightlessSync.Services.Profiles; namespace LightlessSync.UI; -public class EditProfileUi : WindowMediatorSubscriberBase +public partial class EditProfileUi : WindowMediatorSubscriberBase { private readonly ApiController _apiController; private readonly FileDialogManager _fileDialogManager; private readonly LightlessProfileManager _lightlessProfileManager; private readonly UiSharedService _uiSharedService; - private bool _adjustedForScollBarsLocalProfile = false; - private bool _adjustedForScollBarsOnlineProfile = false; + private readonly ProfileTagService _profileTagService; + private const string LoadingProfileDescription = "Loading Profile Data from server..."; + private const string DescriptionMacroTooltip = + "Supported SeString markup:\n" + + "
- insert a line break (Enter already emits these).\n" + + "<-> - optional soft hyphen / word break.\n" + + " / - show game icons by numeric id;" + + "text or - tint text; reset with or .\n" + + " / - use UI palette colours (0 restores defaults).\n" + + " / - change outline colour.\n" + + " - toggle style flags.\n" + + " - create clickable links."; + + private static readonly HashSet _supportedImageExtensions = new(StringComparer.OrdinalIgnoreCase) + { + "png", + "jpg", + "jpeg", + "webp", + "bmp" + }; + private const string _imageFileDialogFilter = "Images{.png,.jpg,.jpeg,.webp,.bmp}"; + private readonly List _tagEditorSelection; + private int[] _profileTagIds = []; + private readonly List _tagPreviewSegments = new(); + private enum ProfileEditorMode + { + User, + Group + } + + private ProfileEditorMode _mode = ProfileEditorMode.User; + private GroupFullInfoDto? _groupInfo; + private LightlessGroupProfileData? _groupProfileData; + private bool _groupIsNsfw; + private bool _groupIsDisabled; + private bool _groupServerIsNsfw; + private bool _groupServerIsDisabled; + private bool _groupVisibilityInitialized; + private byte[]? _queuedProfileImage; + private byte[]? _queuedBannerImage; + private readonly Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f); + private readonly Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f); + private const int _maxProfileTags = 12; + private const int _availableTagsPerPage = 6; + private int _availableTagPage; + private UserData? _selfProfileUserData; private string _descriptionText = string.Empty; private IDalamudTextureWrap? _pfpTextureWrap; + private IDalamudTextureWrap? _bannerTextureWrap; private string _profileDescription = string.Empty; private byte[] _profileImage = []; - private bool _showFileDialogError = false; + private byte[] _bannerImage = []; + private bool _showProfileImageError = false; + private bool _showBannerImageError = false; private bool _wasOpen; + private bool _userServerIsNsfw; + private bool _isNsfwInitialized; + private bool _isNsfw; private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f); - private bool vanityInitialized; // useless for now - private bool textEnabled; - private bool glowEnabled; - private Vector4 textColor; - private Vector4 glowColor; + private bool _textEnabled; + private bool _glowEnabled; + private Vector4 _textColor; + private Vector4 _glowColor; - private record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor); - private VanityState _savedVanity; + private sealed record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor); + private VanityState? _savedVanity; public EditProfileUi(ILogger logger, LightlessMediator mediator, ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager, - LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService) + LightlessProfileManager lightlessProfileManager, ProfileTagService profileTagService, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "Lightless Sync Edit Profile###LightlessSyncEditProfileUI", performanceCollectorService) { IsOpen = false; - this.SizeConstraints = new() + var scale = ImGuiHelpers.GlobalScale; + var editorSize = ProfileEditorLayoutCoordinator.GetEditorSize(scale); + Size = editorSize; + SizeCondition = ImGuiCond.FirstUseEver; + SizeConstraints = new() { - MinimumSize = new(850, 640), - MaximumSize = new(850, 700) + MinimumSize = editorSize, + MaximumSize = editorSize }; + Flags |= ImGuiWindowFlags.NoResize; _apiController = apiController; _uiSharedService = uiSharedService; _fileDialogManager = fileDialogManager; _lightlessProfileManager = lightlessProfileManager; + _profileTagService = profileTagService; + _tagEditorSelection = new List(_maxProfileTags); Mediator.Subscribe(this, (_) => { _wasOpen = IsOpen; IsOpen = false; }); Mediator.Subscribe(this, (_) => IsOpen = _wasOpen); - Mediator.Subscribe(this, (_) => IsOpen = false); + Mediator.Subscribe(this, (_) => + { + IsOpen = false; + _selfProfileUserData = null; + }); Mediator.Subscribe(this, (msg) => { if (msg.UserData == null || string.Equals(msg.UserData.UID, _apiController.UID, StringComparison.Ordinal)) @@ -73,325 +139,1124 @@ public class EditProfileUi : WindowMediatorSubscriberBase }); Mediator.Subscribe(this, msg => { + _selfProfileUserData = msg.Connection.User with + { + IsAdmin = msg.Connection.IsAdmin, + IsModerator = msg.Connection.IsModerator, + HasVanity = msg.Connection.HasVanity, + TextColorHex = msg.Connection.TextColorHex, + TextGlowColorHex = msg.Connection.TextGlowColorHex + }; LoadVanity(); }); + Mediator.Subscribe(this, msg => OpenGroupEditor(msg.Group)); } private void LoadVanity() { - textEnabled = !string.IsNullOrEmpty(_apiController.TextColorHex); - glowEnabled = !string.IsNullOrEmpty(_apiController.TextGlowColorHex); + _textEnabled = !string.IsNullOrEmpty(_apiController.TextColorHex); + _glowEnabled = !string.IsNullOrEmpty(_apiController.TextGlowColorHex); - textColor = textEnabled ? UIColors.HexToRgba(_apiController.TextColorHex!) : Vector4.One; - glowColor = glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero; + _textColor = _textEnabled ? UIColors.HexToRgba(_apiController.TextColorHex!) : Vector4.One; + _glowColor = _glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero; - _savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor); - vanityInitialized = true; + _savedVanity = new VanityState(_textEnabled, _glowEnabled, _textColor, _glowColor); + } + + public override async void OnOpen() + { + if (_mode == ProfileEditorMode.Group) + { + if (_groupInfo is not null) + { + var scale = ImGuiHelpers.GlobalScale; + var viewport = ImGui.GetMainViewport(); + ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale); + } + return; + } + + _isNsfwInitialized = false; + + var user = await EnsureSelfProfileUserDataAsync().ConfigureAwait(false); + if (user is not null) + { + ProfileEditorLayoutCoordinator.Enable(user.UID); + var scale = ImGuiHelpers.GlobalScale; + var viewport = ImGui.GetMainViewport(); + ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale); + Mediator.Publish(new OpenSelfProfilePreviewMessage(user)); + } + } + + public override void OnClose() + { + if (_mode == ProfileEditorMode.Group) + { + if (_groupInfo is not null) + { + ProfileEditorLayoutCoordinator.Disable(_groupInfo.Group.GID); + Mediator.Publish(new CloseGroupProfilePreviewMessage(_groupInfo)); + } + + ResetGroupEditorState(); + _mode = ProfileEditorMode.User; + return; + } + + if (_selfProfileUserData is not null) + { + ProfileEditorLayoutCoordinator.Disable(_selfProfileUserData.UID); + Mediator.Publish(new CloseSelfProfilePreviewMessage(_selfProfileUserData)); + } + } + + private async Task EnsureSelfProfileUserDataAsync() + { + if (_selfProfileUserData is not null) + return _selfProfileUserData; + + try + { + var connection = await _apiController.GetConnectionDtoAsync(publishConnected: false).ConfigureAwait(false); + _selfProfileUserData = connection.User with + { + IsAdmin = connection.IsAdmin, + IsModerator = connection.IsModerator, + HasVanity = connection.HasVanity, + TextColorHex = connection.TextColorHex, + TextGlowColorHex = connection.TextGlowColorHex + }; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to acquire connection information for profile preview."); + } + + return _selfProfileUserData; } protected override void DrawInternal() { + var scale = ImGuiHelpers.GlobalScale; - _uiSharedService.UnderlinedBigText("Notes and Rules for Profiles", UIColors.Get("LightlessYellow")); - ImGui.Dummy(new Vector2(5)); + if (_mode == ProfileEditorMode.Group) + { + DrawGroupEditor(scale); + return; + } - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 1)); + var viewport = ImGui.GetMainViewport(); + var linked = _selfProfileUserData is not null && ProfileEditorLayoutCoordinator.IsActive(_selfProfileUserData.UID); - _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "All users that are paired and unpaused with you will be able to see your profile picture and description."); - _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Other users have the possibility to report your profile for breaking the rules."); - _uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.)"); - _uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Slurs of any kind in the description that can be considered highly offensive"); - _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely."); - _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent."); - _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"), "If your profile picture or profile description could be considered NSFW, enable the toggle in profile settings."); + if (linked) + { + ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale); - ImGui.PopStyleVar(); + var desiredSize = ProfileEditorLayoutCoordinator.GetEditorSize(scale); + if (!ProfileEditorLayoutCoordinator.NearlyEquals(ImGui.GetWindowSize(), desiredSize)) + ImGui.SetWindowSize(desiredSize, ImGuiCond.Always); - ImGui.Dummy(new Vector2(3)); + var currentPos = ImGui.GetWindowPos(); + if (IsWindowBeingDragged()) + ProfileEditorLayoutCoordinator.UpdateAnchorFromEditor(currentPos, scale); + + var desiredPos = ProfileEditorLayoutCoordinator.GetEditorPosition(scale); + if (!ProfileEditorLayoutCoordinator.NearlyEquals(currentPos, desiredPos)) + ImGui.SetWindowPos(desiredPos, ImGuiCond.Always); + } + else + { + var defaultProfilePos = viewport.WorkPos + new Vector2(50f, 70f) * scale; + var defaultEditorPos = defaultProfilePos + ProfileEditorLayoutCoordinator.GetEditorOffset(scale); + ImGui.SetWindowPos(defaultEditorPos, ImGuiCond.FirstUseEver); + } var profile = _lightlessProfileManager.GetLightlessUserProfile(new UserData(_apiController.UID)); - _logger.LogInformation("Profile fetched for drawing: {profile}", profile); - if (ImGui.BeginTabBar("##EditProfileTabs")) - { - if (ImGui.BeginTabItem("Current Profile")) + var accent = UIColors.Get("LightlessPurple"); + var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.015f); + var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.07f); + + using var panelBg = ImRaii.PushColor(ImGuiCol.ChildBg, accentBg); + using var panelBorder = ImRaii.PushColor(ImGuiCol.ChildBg, accentBorder); + ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale); + + if (ImGui.BeginChild("##ProfileEditorCanvas", -Vector2.One, true)) + { + DrawGuidelinesSection(scale); + ImGui.Dummy(new Vector2(0f, 4f * scale)); + DrawTabInterface(profile, scale); + } + ImGui.EndChild(); + ImGui.PopStyleVar(); + } + + private void DrawGuidelinesSection(float scale) + { + DrawSection("Guidelines", scale, () => + { + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 1)); + + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "All users that are paired and unpaused with you will be able to see your profile pictures, tags and description."); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Other users have the possibility to report your profile for breaking the rules."); + _uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.)"); + _uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Slurs of any kind in the description that can be considered highly offensive"); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely."); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent."); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"), "If your profile picture or profile description could be considered NSFW, enable the toggle in visibility settings."); + + ImGui.PopStyleVar(); + }); + } + + private void DrawTabInterface(LightlessUserProfileData profile, float scale) + { + ImGui.PushStyleVar(ImGuiStyleVar.TabRounding, 4f * scale); + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(8f, 4f) * scale); + + if (ImGui.BeginTabBar("##ProfileEditorTabs", ImGuiTabBarFlags.NoCloseWithMiddleMouseButton | ImGuiTabBarFlags.FittingPolicyResizeDown)) + { + if (ImGui.BeginTabItem("Profile")) { - _uiSharedService.MediumText("Current Profile (as saved on server)", UIColors.Get("LightlessPurple")); - ImGui.Dummy(new Vector2(5)); - - if (profile.IsFlagged) - { - UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed); - return; - } - - if (!_profileImage.SequenceEqual(profile.ImageData.Value)) - { - _profileImage = profile.ImageData.Value; - _pfpTextureWrap?.Dispose(); - _pfpTextureWrap = _uiSharedService.LoadImage(_profileImage); - } - - if (!string.Equals(_profileDescription, profile.Description, StringComparison.OrdinalIgnoreCase)) - { - _profileDescription = profile.Description; - _descriptionText = _profileDescription; - } - - if (_pfpTextureWrap != null) - { - ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height)); - } - - var spacing = ImGui.GetStyle().ItemSpacing.X; - ImGuiHelpers.ScaledRelativeSameLine(256, spacing); - using (_uiSharedService.GameFont.Push()) - { - var descriptionTextSize = ImGui.CalcTextSize(profile.Description, wrapWidth: 256f); - var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256); - if (descriptionTextSize.Y > childFrame.Y) - { - _adjustedForScollBarsOnlineProfile = true; - } - else - { - _adjustedForScollBarsOnlineProfile = false; - } - childFrame = childFrame with - { - X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0), - }; - if (ImGui.BeginChildFrame(101, childFrame)) - { - UiSharedService.TextWrapped(profile.Description); - } - ImGui.EndChildFrame(); - } - - var nsfw = profile.IsNSFW; - ImGui.BeginDisabled(); - ImGui.Checkbox("Is NSFW", ref nsfw); - ImGui.EndDisabled(); - - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + DrawProfileTabContent(profile, scale); ImGui.EndTabItem(); } - if (ImGui.BeginTabItem("Profile Settings")) + if (ImGui.BeginTabItem("Vanity")) { - _uiSharedService.MediumText("Profile Settings", UIColors.Get("LightlessPurple")); - ImGui.Dummy(new Vector2(5)); - - if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) - { - _fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) => - { - if (!success) return; - _ = Task.Run(async () => - { - var fileContent = File.ReadAllBytes(file); - using MemoryStream ms = new(fileContent); - var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); - if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) - { - _showFileDialogError = true; - return; - } - using var image = Image.Load(fileContent); - - if (image.Width > 256 || image.Height > 256 || (fileContent.Length > 250 * 1024)) - { - _showFileDialogError = true; - return; - } - - _showFileDialogError = false; - await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), BannerPictureBase64: null, Description: null, Tags: null)) - .ConfigureAwait(false); - }); - }); - } - UiSharedService.AttachToolTip("Select and upload a new profile picture"); - ImGui.SameLine(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture")) - { - _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null, BannerPictureBase64: null, Tags: null)); - } - UiSharedService.AttachToolTip("Clear your currently uploaded profile picture"); - if (_showFileDialogError) - { - UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed); - } - var isNsfw = profile.IsNSFW; - if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) - { - _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null, BannerPictureBase64: null, Tags: null)); - } - _uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON"); - var widthTextBox = 400; - var posX = ImGui.GetCursorPosX(); - ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500"); - ImGui.SetCursorPosX(posX); - ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X); - ImGui.TextUnformatted("Preview (approximate)"); - using (_uiSharedService.GameFont.Push()) - ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200)); - - ImGui.SameLine(); - - using (_uiSharedService.GameFont.Push()) - { - var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f); - var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200); - if (descriptionTextSizeLocal.Y > childFrameLocal.Y) - { - _adjustedForScollBarsLocalProfile = true; - } - else - { - _adjustedForScollBarsLocalProfile = false; - } - childFrameLocal = childFrameLocal with - { - X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0), - }; - if (ImGui.BeginChildFrame(102, childFrameLocal)) - { - UiSharedService.TextWrapped(_descriptionText); - } - ImGui.EndChildFrame(); - } - - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description")) - { - _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, _descriptionText, Tags: null)); - } - UiSharedService.AttachToolTip("Sets your profile description text"); - ImGui.SameLine(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description")) - { - _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, BannerPictureBase64: null, "", Tags: null)); - } - UiSharedService.AttachToolTip("Clears your profile description text"); - - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Vanity Settings")) - { - _uiSharedService.MediumText("Supporter Vanity Settings", UIColors.Get("LightlessPurple")); - ImGui.Dummy(new Vector2(4)); - _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings. If you have the vanity role, you must interact with the Discord bot first."); - - var hasVanity = _apiController.HasVanity; - - if (!hasVanity) - { - UiSharedService.ColorTextWrapped("You do not currently have vanity access. Become a supporter to unlock these features.", UIColors.Get("DimRed")); - ImGui.Dummy(new Vector2(8)); - ImGui.BeginDisabled(); - } - - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - _uiSharedService.MediumText("Colored UID", UIColors.Get("LightlessPurple")); - ImGui.Dummy(new Vector2(5)); - - var font = UiBuilder.MonoFont; - var playerUID = _apiController.UID; - var playerDisplay = _apiController.DisplayName; - - var previewTextColor = textEnabled ? textColor : Vector4.One; - var previewGlowColor = glowEnabled ? glowColor : Vector4.Zero; - - var seString = SeStringUtils.BuildFormattedPlayerName(playerDisplay, previewTextColor, previewGlowColor); - - using (ImRaii.PushFont(font)) - { - var drawList = ImGui.GetWindowDrawList(); - var textSize = ImGui.CalcTextSize(seString.TextValue); - - float minWidth = 150f * ImGuiHelpers.GlobalScale; - float bgWidth = Math.Max(textSize.X + 20f, minWidth); - - float paddingY = 5f * ImGuiHelpers.GlobalScale; - - var cursor = ImGui.GetCursorScreenPos(); - - var rectMin = cursor; - var rectMax = rectMin + new Vector2(bgWidth, textSize.Y + (paddingY * 2f)); - - float boost = Luminance.ComputeHighlight(previewTextColor, previewGlowColor); - - var baseBg = new Vector4(0.15f + boost, 0.15f + boost, 0.15f + boost, 1f); - var bgColor = Luminance.BackgroundContrast(previewTextColor, previewGlowColor, baseBg, ref _currentBg); - - var borderColor = UIColors.Get("LightlessPurple"); - - drawList.AddRectFilled(rectMin, rectMax, ImGui.GetColorU32(bgColor), 6.0f); - drawList.AddRect(rectMin, rectMax, ImGui.GetColorU32(borderColor), 6.0f, ImDrawFlags.None, 1.5f); - - var textPos = new Vector2( - rectMin.X + (bgWidth - textSize.X) * 0.5f, - rectMin.Y + paddingY - ); - - SeStringUtils.RenderSeStringWithHitbox(seString, textPos, font); - - ImGui.Dummy(new Vector2(5)); - } - - const float colorPickAlign = 90f; - - _uiSharedService.DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Text Color"); - ImGui.SameLine(colorPickAlign); - ImGui.Checkbox("##toggleTextColor", ref textEnabled); - ImGui.SameLine(); - ImGui.BeginDisabled(!textEnabled); - ImGui.ColorEdit4($"##color_text", ref textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); - ImGui.EndDisabled(); - - _uiSharedService.DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Glow Color"); - ImGui.SameLine(colorPickAlign); - ImGui.Checkbox("##toggleGlowColor", ref glowEnabled); - ImGui.SameLine(); - ImGui.BeginDisabled(!glowEnabled); - ImGui.ColorEdit4($"##color_glow", ref glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); - ImGui.EndDisabled(); - - bool changed = !Equals(_savedVanity, new VanityState(textEnabled, glowEnabled, textColor, glowColor)); - - if (!changed) - ImGui.BeginDisabled(); - - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Changes")) - { - string? newText = textEnabled ? UIColors.RgbaToHex(textColor) : string.Empty; - string? newGlow = glowEnabled ? UIColors.RgbaToHex(glowColor) : string.Empty; - - _ = _apiController.UserUpdateVanityColors(new UserVanityColorsDto(newText, newGlow)); - - _savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor); - } - - if (!changed) - ImGui.EndDisabled(); - - ImGui.Dummy(new Vector2(5)); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - - if (!hasVanity) - ImGui.EndDisabled(); - + DrawVanityTabContent(scale); ImGui.EndTabItem(); } ImGui.EndTabBar(); } + ImGui.PopStyleVar(2); + } + + private void DrawProfileTabContent(LightlessUserProfileData profile, float scale) + { + if (profile.IsFlagged) + { + DrawSection("Moderation Status", scale, () => + { + UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed); + }); + return; + } + + SyncProfileState(profile); + DrawSection("Profile Preview", scale, () => DrawProfileSnapshot(profile, scale)); + DrawSection("Profile Image", scale, () => DrawProfileImageControls(profile)); + DrawSection("Profile Banner", scale, () => DrawProfileBannerControls(profile)); + DrawSection("Profile Description", scale, () => DrawProfileDescriptionEditor(profile, scale)); + DrawSection("Profile Tags", scale, () => DrawProfileTagsEditor(profile, scale)); + DrawSection("Visibility", scale, () => DrawProfileVisibilityControls()); + } + + private void DrawProfileSnapshot(LightlessUserProfileData profile, float scale) + { + var bannerHeight = 140f * scale; + ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale); + if (ImGui.BeginChild("##ProfileBannerPreview", new Vector2(-1f, bannerHeight), true)) + { + if (_bannerTextureWrap != null) + { + var childSize = ImGui.GetWindowSize(); + var padding = ImGui.GetStyle().WindowPadding; + var contentSize = new Vector2( + MathF.Max(childSize.X - padding.X * 2f, 1f), + MathF.Max(childSize.Y - padding.Y * 2f, 1f)); + + var imageSize = ImGuiHelpers.ScaledVector2(_bannerTextureWrap.Width, _bannerTextureWrap.Height); + if (imageSize.X > contentSize.X || imageSize.Y > contentSize.Y) + { + var ratio = MathF.Min(contentSize.X / MathF.Max(imageSize.X, 1f), contentSize.Y / MathF.Max(imageSize.Y, 1f)); + imageSize *= ratio; + } + + var offset = new Vector2( + MathF.Max((contentSize.X - imageSize.X) * 0.5f, 0f), + MathF.Max((contentSize.Y - imageSize.Y) * 0.5f, 0f)); + ImGui.SetCursorPos(padding + offset); + ImGui.Image(_bannerTextureWrap.Handle, imageSize); + } + else + { + ImGui.TextColored(UIColors.Get("LightlessPurple"), "No Profile Banner"); + } + } + ImGui.EndChild(); + ImGui.PopStyleVar(); + + ImGui.Dummy(new Vector2(0f, 6f * scale)); + + if (_pfpTextureWrap != null) + { + var size = ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height); + var maxEdge = 150f * scale; + if (size.X > maxEdge || size.Y > maxEdge) + { + var ratio = MathF.Min(maxEdge / MathF.Max(size.X, 1f), maxEdge / MathF.Max(size.Y, 1f)); + size *= ratio; + } + + ImGui.Image(_pfpTextureWrap.Handle, size); + } + else + { + ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale); + if (ImGui.BeginChild("##ProfileImagePlaceholder", new Vector2(150f * scale, 150f * scale), true)) + ImGui.TextColored(UIColors.Get("LightlessPurple"), "No Profile Picture"); + ImGui.EndChild(); + ImGui.PopStyleVar(); + } + + ImGui.Dummy(new Vector2(0f, 4f * scale)); + using (_uiSharedService.GameFont.Push()) + { + ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale); + if (ImGui.BeginChild("##CurrentProfileDescription", new Vector2(-1f, 120f * scale), true)) + { + ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X); + if (string.IsNullOrWhiteSpace(profile.Description)) + { + ImGui.TextDisabled("-- No description --"); + } + else if (!SeStringUtils.TryRenderSeStringMarkupAtCursor(profile.Description!)) + { + UiSharedService.TextWrapped(profile.Description); + } + ImGui.PopTextWrapPos(); + } + ImGui.EndChild(); + ImGui.PopStyleVar(); + } + + } + + private void DrawProfileImageControls(LightlessUserProfileData profile) + { + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "Profile pictures must be 512x512 and under 2 MiB."); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) + { + var existingBanner = GetCurrentProfileBannerBase64(profile); + _fileDialogManager.OpenFileDialog("Select new Profile picture", _imageFileDialogFilter, (success, file) => + { + if (!success) return; + _ = Task.Run(async () => + { + var fileContent = File.ReadAllBytes(file); + using MemoryStream ms = new(fileContent); + var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); + if (!IsSupportedImageFormat(format)) + { + _showProfileImageError = true; + return; + } + + using var image = Image.Load(fileContent); + if (image.Width > 512 || image.Height > 512 || fileContent.Length > 2000 * 1024) + { + _showProfileImageError = true; + return; + } + + _showProfileImageError = false; + var currentTags = GetServerTagPayload(); + _queuedProfileImage = fileContent; + await _apiController.UserSetProfile(new UserProfileDto( + new UserData(_apiController.UID), + Disabled: false, + IsNSFW: null, + ProfilePictureBase64: Convert.ToBase64String(fileContent), + BannerPictureBase64: existingBanner, + Description: null, + Tags: currentTags)).ConfigureAwait(false); + }); + }); + } + UiSharedService.AttachToolTip("Select an image up to 512x512 pixels and <= 2 MiB (PNG/JPG/JPEG/WEBP/BMP)."); + + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear profile picture")) + { + _ = _apiController.UserSetProfile(new UserProfileDto( + new UserData(_apiController.UID), + Disabled: false, + IsNSFW: null, + ProfilePictureBase64: string.Empty, + BannerPictureBase64: GetCurrentProfileBannerBase64(profile), + Description: null, + Tags: GetServerTagPayload())); + } + UiSharedService.AttachToolTip("Remove your current profile picture."); + + if (_showProfileImageError) + { + UiSharedService.ColorTextWrapped("Your profile picture must be no larger than 512x512 pixels and under 2 MiB.", ImGuiColors.DalamudRed); + } + } + + private void DrawProfileBannerControls(LightlessUserProfileData profile) + { + _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "Profile banners must be 840x260 and under 2 MiB."); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile banner")) + { + var existingProfile = GetCurrentProfilePictureBase64(profile); + _fileDialogManager.OpenFileDialog("Select new Profile banner", _imageFileDialogFilter, (success, file) => + { + if (!success) return; + _ = Task.Run(async () => + { + var fileContent = File.ReadAllBytes(file); + using MemoryStream ms = new(fileContent); + var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); + if (!IsSupportedImageFormat(format)) + { + _showBannerImageError = true; + return; + } + + using var image = Image.Load(fileContent); + if (image.Width > 840 || image.Height > 260 || fileContent.Length > 2000 * 1024) + { + _showBannerImageError = true; + return; + } + + _showBannerImageError = false; + var currentTags = GetServerTagPayload(); + await _apiController.UserSetProfile(new UserProfileDto( + new UserData(_apiController.UID), + Disabled: false, + IsNSFW: null, + ProfilePictureBase64: existingProfile, + BannerPictureBase64: Convert.ToBase64String(fileContent), + Description: null, + Tags: currentTags)).ConfigureAwait(false); + }); + }); + } + UiSharedService.AttachToolTip("Select an image up to 840x260 pixels and <= 2 MiB (PNG/JPG/JPEG/WEBP/BMP)."); + + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear profile banner")) + { + _ = _apiController.UserSetProfile(new UserProfileDto( + new UserData(_apiController.UID), + Disabled: false, + IsNSFW: null, + ProfilePictureBase64: GetCurrentProfilePictureBase64(profile), + BannerPictureBase64: string.Empty, + Description: null, + Tags: GetServerTagPayload())); + } + UiSharedService.AttachToolTip("Remove your current profile banner."); + + if (_showBannerImageError) + { + UiSharedService.ColorTextWrapped("Your banner image must be no larger than 840x260 pixels and under 2 MiB.", ImGuiColors.DalamudRed); + } + } + + private void DrawProfileDescriptionEditor(LightlessUserProfileData profile, float scale) + { + ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500"); + ImGui.SameLine(); + ImGuiComponents.HelpMarker(DescriptionMacroTooltip); + using (_uiSharedService.GameFont.Push()) + { + var inputSize = new Vector2(-1f, 160f * scale); + ImGui.InputTextMultiline("##profileDescriptionInput", ref _descriptionText, 1500, inputSize); + } + + ImGui.Dummy(new Vector2(0f, 3f * scale)); + ImGui.TextColored(UIColors.Get("LightlessBlue"), "Preview"); + using (_uiSharedService.GameFont.Push()) + { + ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 4f * scale); + if (ImGui.BeginChild("##profileDescriptionPreview", new Vector2(-1f, 140f * scale), true)) + { + ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X); + if (string.IsNullOrWhiteSpace(_descriptionText)) + { + ImGui.TextDisabled("-- Description preview --"); + } + else if (!SeStringUtils.TryRenderSeStringMarkupAtCursor(_descriptionText)) + { + UiSharedService.TextWrapped(_descriptionText); + } + ImGui.PopTextWrapPos(); + } + ImGui.EndChild(); + ImGui.PopStyleVar(); + } + + ImGui.Dummy(new Vector2(0f, 4f * scale)); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description")) + { + _ = _apiController.UserSetProfile(new UserProfileDto( + new UserData(_apiController.UID), + Disabled: false, + IsNSFW: null, + ProfilePictureBase64: GetCurrentProfilePictureBase64(profile), + BannerPictureBase64: GetCurrentProfileBannerBase64(profile), + _descriptionText, + Tags: GetServerTagPayload())); + } + UiSharedService.AttachToolTip("Apply the text above to your profile."); + + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description")) + { + _descriptionText = string.Empty; + _ = _apiController.UserSetProfile(new UserProfileDto( + new UserData(_apiController.UID), + Disabled: false, + IsNSFW: null, + ProfilePictureBase64: GetCurrentProfilePictureBase64(profile), + BannerPictureBase64: GetCurrentProfileBannerBase64(profile), + Description: string.Empty, + Tags: GetServerTagPayload())); + } + UiSharedService.AttachToolTip("Remove the description from your profile."); + } + + private void DrawProfileTagsEditor(LightlessUserProfileData profile, float scale) + { + DrawTagEditor( + scale, + contextPrefix: "user", + saveTooltip: "Apply the selected tags to your profile.", + submitAction: payload => _apiController.UserSetProfile(new UserProfileDto( + new UserData(_apiController.UID), + Disabled: false, + IsNSFW: null, + ProfilePictureBase64: GetCurrentProfilePictureBase64(profile), + BannerPictureBase64: GetCurrentProfileBannerBase64(profile), + Description: null, + Tags: payload)), + allowReorder: true, + sortPayloadBeforeSubmit: false); + } + + private void DrawTagEditor( + float scale, + string contextPrefix, + string saveTooltip, + Func submitAction, + bool allowReorder, + bool sortPayloadBeforeSubmit, + Action? onPayloadPrepared = null) + { + var tagLibrary = ProfileTagService.GetTagLibrary(); + if (tagLibrary.Count == 0) + { + ImGui.TextDisabled("No profile tags are available."); + return; + } + + var style = ImGui.GetStyle(); + var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text); + + var selectedCount = _tagEditorSelection.Count; + ImGui.TextColored(UIColors.Get("LightlessBlue"), $"Selected Tags ({selectedCount}/{_maxProfileTags})"); + + int? tagToRemove = null; + int? moveUpRequest = null; + int? moveDownRequest = null; + + if (selectedCount == 0) + { + ImGui.TextDisabled("-- No tags selected --"); + } + else + { + var selectedFlags = ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.SizingStretchSame; + var selectedTableId = $"##{contextPrefix}SelectedTagsTable"; + var columnCount = allowReorder ? 3 : 2; + + if (ImGui.BeginTable(selectedTableId, columnCount, selectedFlags)) + { + ImGui.TableSetupColumn("Preview", ImGuiTableColumnFlags.WidthStretch, allowReorder ? 0.55f : 0.75f); + if (allowReorder) + ImGui.TableSetupColumn("##order", ImGuiTableColumnFlags.WidthStretch, 0.1f); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 90f); + ImGui.TableHeadersRow(); + + for (int i = 0; i < _tagEditorSelection.Count; i++) + { + var tagId = _tagEditorSelection[i]; + if (!tagLibrary.TryGetValue(tagId, out var definition) || !definition.HasContent) + continue; + + var displayName = GetTagDisplayName(definition, tagId); + var idLabel = $"ID {tagId}"; + var previewSize = ProfileTagRenderer.MeasureTag(definition, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _tagPreviewSegments, ResolveIconWrap, _logger); + var textHeight = ImGui.CalcTextSize(displayName).Y + style.ItemSpacing.Y + ImGui.CalcTextSize(idLabel).Y; + var rowHeight = MathF.Max(previewSize.Y + style.CellPadding.Y * 2f, textHeight + style.CellPadding.Y * 2f); + + ImGui.TableNextRow(ImGuiTableRowFlags.None, rowHeight); + ImGui.TableNextColumn(); + using (ImRaii.PushId($"{contextPrefix}-selected-tag-{tagId}-{i}")) + DrawCenteredTagCell(definition, scale, previewSize, rowHeight, defaultTextColorU32); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.TextUnformatted(displayName); + ImGui.TextDisabled(idLabel); + ImGui.EndTooltip(); + } + + if (allowReorder) + { + ImGui.TableNextColumn(); + DrawReorderCell(contextPrefix, tagId, i, _tagEditorSelection.Count, rowHeight, scale, ref moveUpRequest, ref moveDownRequest); + } + + ImGui.TableNextColumn(); + DrawFullCellButton("Remove", UIColors.Get("DimRed"), ref tagToRemove, tagId, false, scale, rowHeight, $"{contextPrefix}-remove-{tagId}-{i}"); + } + + ImGui.EndTable(); + } + } + + if (allowReorder) + { + if (moveUpRequest.HasValue && moveUpRequest.Value > 0) + { + var idx = moveUpRequest.Value; + (_tagEditorSelection[idx - 1], _tagEditorSelection[idx]) = (_tagEditorSelection[idx], _tagEditorSelection[idx - 1]); + } + + if (moveDownRequest.HasValue && moveDownRequest.Value < _tagEditorSelection.Count - 1) + { + var idx = moveDownRequest.Value; + (_tagEditorSelection[idx], _tagEditorSelection[idx + 1]) = (_tagEditorSelection[idx + 1], _tagEditorSelection[idx]); + } + } + + if (tagToRemove.HasValue) + _tagEditorSelection.Remove(tagToRemove.Value); + + bool limitReached = _tagEditorSelection.Count >= _maxProfileTags; + if (limitReached) + UiSharedService.ColorTextWrapped($"You have reached the maximum of {_maxProfileTags} tags. Remove one before adding more.", UIColors.Get("DimRed")); + + ImGui.Dummy(new Vector2(0f, 6f * scale)); + ImGui.TextColored(UIColors.Get("LightlessPurple"), "Available Tags"); + + var availableIds = new List(tagLibrary.Count); + var seenDefinitions = new HashSet(); + foreach (var kvp in tagLibrary) + { + var definition = kvp.Value; + if (!definition.HasContent) + continue; + + if (!seenDefinitions.Add(definition)) + continue; + + if (_tagEditorSelection.Contains(kvp.Key)) + continue; + + availableIds.Add(kvp.Key); + } + + availableIds.Sort(); + int totalAvailable = availableIds.Count; + if (totalAvailable == 0) + { + ImGui.TextDisabled("-- No additional tags available --"); + } + else + { + int pageCount = Math.Max(1, (totalAvailable + _availableTagsPerPage - 1) / _availableTagsPerPage); + _availableTagPage = Math.Clamp(_availableTagPage, 0, pageCount - 1); + int start = _availableTagPage * _availableTagsPerPage; + int end = Math.Min(totalAvailable, start + _availableTagsPerPage); + + ImGui.SameLine(); + ImGui.TextDisabled($"Page {_availableTagPage + 1}/{pageCount}"); + ImGui.SameLine(); + ImGui.BeginDisabled(_availableTagPage == 0); + if (ImGui.SmallButton($"<##{contextPrefix}TagPagePrev")) + _availableTagPage--; + ImGui.EndDisabled(); + ImGui.SameLine(); + ImGui.BeginDisabled(_availableTagPage >= pageCount - 1); + if (ImGui.SmallButton($">##{contextPrefix}TagPageNext")) + _availableTagPage++; + ImGui.EndDisabled(); + + var availableFlags = ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.SizingStretchSame; + int? tagToAdd = null; + + if (ImGui.BeginTable($"##{contextPrefix}AvailableTagsTable", 2, availableFlags)) + { + ImGui.TableSetupColumn("Preview", ImGuiTableColumnFlags.WidthStretch, 0.75f); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 90f); + ImGui.TableHeadersRow(); + + for (int idx = start; idx < end; idx++) + { + var tagId = availableIds[idx]; + if (!tagLibrary.TryGetValue(tagId, out var definition) || !definition.HasContent) + continue; + + var previewSize = ProfileTagRenderer.MeasureTag(definition, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _tagPreviewSegments, ResolveIconWrap, _logger); + var rowHeight = previewSize.Y + style.CellPadding.Y * 2f; + + ImGui.TableNextRow(ImGuiTableRowFlags.None, rowHeight); + ImGui.TableNextColumn(); + using (ImRaii.PushId($"{contextPrefix}-available-tag-{tagId}")) + DrawCenteredTagCell(definition, scale, previewSize, rowHeight, defaultTextColorU32); + if (ImGui.IsItemHovered()) + { + var name = GetTagDisplayName(definition, tagId); + ImGui.BeginTooltip(); + ImGui.TextUnformatted(name); + ImGui.TextDisabled($"ID {tagId}"); + ImGui.EndTooltip(); + } + + ImGui.TableNextColumn(); + DrawFullCellButton("Add", UIColors.Get("LightlessGreen"), ref tagToAdd, tagId, limitReached, scale, rowHeight, $"{contextPrefix}-add-{tagId}"); + } + + ImGui.EndTable(); + } + + if (tagToAdd.HasValue) + { + _tagEditorSelection.Add(tagToAdd.Value); + if (_availableTagPage > 0 && (totalAvailable - 1) <= start) + _availableTagPage = Math.Max(0, _availableTagPage - 1); + } + } + + bool hasChanges = !TagsEqual(_tagEditorSelection, _profileTagIds); + ImGui.Dummy(new Vector2(0f, 6f * scale)); + if (!hasChanges) + ImGui.BeginDisabled(); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, $"Save Tags##{contextPrefix}")) + { + var payload = _tagEditorSelection.Count == 0 ? Array.Empty() : _tagEditorSelection.ToArray(); + if (sortPayloadBeforeSubmit && payload.Length > 1) + Array.Sort(payload); + onPayloadPrepared?.Invoke(payload); + _ = submitAction(payload); + } + + if (!hasChanges) + ImGui.EndDisabled(); + + UiSharedService.AttachToolTip(saveTooltip); + } + + private void DrawProfileVisibilityControls() + { + if (!_isNsfwInitialized) + ImGui.BeginDisabled(); + + bool changed = DrawCheckboxRow("Mark profile as NSFW", _isNsfw, out var newValue, "Enable when your profile could be considered NSFW."); + + if (changed) + _isNsfw = newValue; + + bool visibilityChanged = _isNsfwInitialized && (_isNsfw != _userServerIsNsfw); + + if (!_isNsfwInitialized) + ImGui.EndDisabled(); + + if (!_isNsfwInitialized || !visibilityChanged) + ImGui.BeginDisabled(); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Apply Visibility Changes")) + { + _userServerIsNsfw = _isNsfw; + + _ = _apiController.UserSetProfile(new UserProfileDto( + new UserData(_apiController.UID), + Disabled: false, + IsNSFW: _isNsfw, + ProfilePictureBase64: null, + BannerPictureBase64: null, + Description: null, + Tags: null)); + } + + UiSharedService.AttachToolTip("Apply the visibility toggles above."); + + if (!_isNsfwInitialized || !visibilityChanged) + ImGui.EndDisabled(); + + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.SyncAlt, "Reset") && _isNsfwInitialized) + _isNsfw = _userServerIsNsfw; + } + + private string? GetCurrentProfilePictureBase64(LightlessUserProfileData profile) + { + if (_queuedProfileImage is { Length: > 0 }) + return Convert.ToBase64String(_queuedProfileImage); + + if (!string.IsNullOrWhiteSpace(profile.Base64ProfilePicture) && !string.Equals(profile.Description, LoadingProfileDescription, StringComparison.Ordinal)) + return profile.Base64ProfilePicture; + + return _profileImage.Length > 0 ? Convert.ToBase64String(_profileImage) : null; + } + + private string? GetCurrentProfileBannerBase64(LightlessUserProfileData profile) + { + if (!string.IsNullOrWhiteSpace(profile.Base64BannerPicture) && !string.Equals(profile.Description, LoadingProfileDescription, StringComparison.Ordinal)) + return profile.Base64BannerPicture; + + return _bannerImage.Length > 0 ? Convert.ToBase64String(_bannerImage) : null; + } + + + private static bool IsSupportedImageFormat(IImageFormat? format) + { + if (format is null) + return false; + + foreach (var ext in format.FileExtensions) + { + if (_supportedImageExtensions.Contains(ext)) + return true; + } + + return false; + } + + private void DrawCenteredTagCell(ProfileTagDefinition tag, float scale, Vector2 tagSize, float rowHeight, uint defaultTextColorU32) + { + var style = ImGui.GetStyle(); + var cellStart = ImGui.GetCursorPos(); + var available = ImGui.GetContentRegionAvail(); + var innerHeight = MathF.Max(0f, rowHeight - style.CellPadding.Y * 2f); + var offsetX = MathF.Max(0f, (available.X - tagSize.X) * 0.5f); + var offsetY = MathF.Max(0f, innerHeight - tagSize.Y) * 0.5f; + + ImGui.SetCursorPos(new Vector2(cellStart.X + offsetX, cellStart.Y + style.CellPadding.Y + offsetY)); + ImGui.InvisibleButton("##tagPreview", tagSize); + var rectMin = ImGui.GetItemRectMin(); + var drawList = ImGui.GetWindowDrawList(); + ProfileTagRenderer.RenderTag(tag, rectMin, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _tagPreviewSegments, ResolveIconWrap, _logger); + } + + private void DrawReorderCell( + string contextPrefix, + int tagId, + int index, + int count, + float rowHeight, + float scale, + ref int? moveUpTarget, + ref int? moveDownTarget) + { + var style = ImGui.GetStyle(); + var cellStart = ImGui.GetCursorPos(); + var availableWidth = ImGui.GetContentRegionAvail().X; + var innerHeight = MathF.Max(0f, rowHeight - style.CellPadding.Y * 2f); + var spacing = MathF.Min(style.ItemSpacing.Y * 0.5f, innerHeight * 0.15f); + var buttonHeight = MathF.Max(1f, (innerHeight - spacing) * 0.5f); + var width = MathF.Max(1f, availableWidth); + + var upColor = UIColors.Get("LightlessBlue"); + using (ImRaii.PushId($"{contextPrefix}-order-{tagId}-{index}")) + { + ImGui.SetCursorPos(new Vector2(cellStart.X, cellStart.Y + style.CellPadding.Y)); + if (ColoredButton("\u2191##tagMoveUp", upColor, new Vector2(width, buttonHeight), scale, index == 0)) + moveUpTarget = index; + + ImGui.SetCursorPos(new Vector2(cellStart.X, cellStart.Y + style.CellPadding.Y + buttonHeight + spacing)); + if (ColoredButton("\u2193##tagMoveDown", upColor, new Vector2(width, buttonHeight), scale, index >= count - 1)) + moveDownTarget = index; + } + } + + private void DrawFullCellButton(string label, Vector4 baseColor, ref int? target, int tagId, bool disabled, float scale, float rowHeight, string idSuffix) + { + var style = ImGui.GetStyle(); + var cellStart = ImGui.GetCursorPos(); + var available = ImGui.GetContentRegionAvail(); + var buttonHeight = MathF.Max(1f, rowHeight - style.CellPadding.Y * 2f); + + ImGui.SetCursorPos(new Vector2(cellStart.X, cellStart.Y + style.CellPadding.Y)); + using (ImRaii.PushId(idSuffix)) + { + if (ColoredButton(label, baseColor, new Vector2(MathF.Max(available.X, 1f), buttonHeight), scale, disabled)) + target = tagId; + } + } + + private static bool ColoredButton(string label, Vector4 baseColor, Vector2 size, float scale, bool disabled) + { + var style = ImGui.GetStyle(); + var hovered = BlendTowardsWhite(baseColor, 0.15f); + var active = BlendTowardsWhite(baseColor, 0.3f); + + ImGui.PushStyleColor(ImGuiCol.Button, baseColor); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hovered); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, active); + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, style.FrameRounding); + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(MathF.Max(2f, style.FramePadding.X), MathF.Max(1f, style.FramePadding.Y * 0.3f)) * scale); + + ImGui.BeginDisabled(disabled); + bool clicked = ImGui.Button(label, size); + ImGui.EndDisabled(); + + ImGui.PopStyleVar(2); + ImGui.PopStyleColor(3); + + return clicked; + } + + private static float Clamp01(float value) + => value < 0f ? 0f : value > 1f ? 1f : value; + + private static Vector4 BlendTowardsWhite(Vector4 color, float amount) + { + var result = Vector4.Lerp(color, Vector4.One, Clamp01(amount)); + result.W = color.W; + return result; + } + + private static string GetTagDisplayName(ProfileTagDefinition tag, int tagId) + { + if (!string.IsNullOrWhiteSpace(tag.Text)) + return tag.Text!; + + if (!string.IsNullOrWhiteSpace(tag.SeStringPayload)) + { + var stripped = SeStringUtils.StripMarkup(tag.SeStringPayload!); + if (!string.IsNullOrWhiteSpace(stripped)) + return stripped; + } + + return $"Tag {tagId}"; + } + + private IDalamudTextureWrap? ResolveIconWrap(uint iconId) + { + if (_uiSharedService.TryGetIcon(iconId, out var wrap) && wrap != null) + return wrap; + return null; + } + + private int[] GetServerTagPayload() + { + if (_profileTagIds.Length == 0) + return Array.Empty(); + + var copy = new int[_profileTagIds.Length]; + Array.Copy(_profileTagIds, copy, _profileTagIds.Length); + return copy; + } + + private static bool TagsEqual(IReadOnlyList? current, IReadOnlyList? reference) + { + if (ReferenceEquals(current, reference)) + return true; + if (current is null || reference is null) + return false; + if (current.Count != reference.Count) + return false; + + for (int i = 0; i < current.Count; i++) + { + if (current[i] != reference[i]) + return false; + } + + return true; + } + + private void DrawVanityTabContent(float scale) + { + DrawSection("Colored UID", scale, () => + { + var hasVanity = _apiController.HasVanity; + if (!hasVanity) + { + UiSharedService.ColorTextWrapped("You do not currently have vanity access. Become a supporter to unlock these features. (If you already are, interact with the bot to update)", UIColors.Get("DimRed")); + } + + var monoFont = UiBuilder.MonoFont; + using (ImRaii.PushFont(monoFont)) + { + var previewTextColor = _textEnabled ? _textColor : Vector4.One; + var previewGlowColor = _glowEnabled ? _glowColor : Vector4.Zero; + var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.DisplayName, previewTextColor, previewGlowColor); + + var drawList = ImGui.GetWindowDrawList(); + var textSize = ImGui.CalcTextSize(seString.TextValue); + float minWidth = 160f * ImGuiHelpers.GlobalScale; + float paddingY = 5f * ImGuiHelpers.GlobalScale; + float paddingX = 10f * ImGuiHelpers.GlobalScale; + float bgWidth = Math.Max(textSize.X + paddingX * 2f, minWidth); + + var style = ImGui.GetStyle(); + var fontHeight = monoFont.FontSize > 0f ? monoFont.FontSize : ImGui.GetFontSize(); + float frameHeight = fontHeight + style.FramePadding.Y * 2f; + float textBlockHeight = MathF.Max(frameHeight, textSize.Y); + + var cursor = ImGui.GetCursorScreenPos(); + var rectMin = cursor; + var rectMax = rectMin + new Vector2(bgWidth, textBlockHeight + paddingY * 2f); + + float boost = Luminance.ComputeHighlight(previewTextColor, previewGlowColor); + + var baseBg = new Vector4(0.15f + boost, 0.15f + boost, 0.15f + boost, 1f); + var bgColor = Luminance.BackgroundContrast(previewTextColor, previewGlowColor, baseBg, ref _currentBg); + var borderColor = UIColors.Get("LightlessPurple"); + + drawList.AddRectFilled(rectMin, rectMax, ImGui.GetColorU32(bgColor), 5f); + drawList.AddRect(rectMin, rectMax, ImGui.GetColorU32(borderColor), 5f, ImDrawFlags.None, 1.2f); + + var textOrigin = new Vector2(rectMin.X + (bgWidth - textSize.X) * 0.5f, rectMin.Y + paddingY); + SeStringUtils.RenderSeStringWithHitbox(seString, textOrigin, monoFont); + + ImGui.Dummy(new Vector2(0f, 1.5f)); + } + + ImGui.TextColored(UIColors.Get("LightlessPurple"), "Colors"); + if (!hasVanity) + ImGui.BeginDisabled(); + + if (DrawCheckboxRow("Enable custom text color", _textEnabled, out var newTextEnabled)) + _textEnabled = newTextEnabled; + + ImGui.SameLine(); + ImGui.BeginDisabled(!_textEnabled); + ImGui.ColorEdit4("Text Color##vanityTextColor", ref _textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); + ImGui.EndDisabled(); + + if (DrawCheckboxRow("Enable glow color", _glowEnabled, out var newGlowEnabled)) + _glowEnabled = newGlowEnabled; + + ImGui.SameLine(); + ImGui.BeginDisabled(!_glowEnabled); + ImGui.ColorEdit4("Glow Color##vanityGlowColor", ref _glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); + ImGui.EndDisabled(); + + bool changed = !Equals(_savedVanity, new VanityState(_textEnabled, _glowEnabled, _textColor, _glowColor)); + if (!changed) + ImGui.BeginDisabled(); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Vanity Changes")) + { + string? newText = _textEnabled ? UIColors.RgbaToHex(_textColor) : string.Empty; + string? newGlow = _glowEnabled ? UIColors.RgbaToHex(_glowColor) : string.Empty; + + _ = _apiController.UserUpdateVanityColors(new UserVanityColorsDto(newText, newGlow)); + _savedVanity = new VanityState(_textEnabled, _glowEnabled, _textColor, _glowColor); + } + + if (!changed) + ImGui.EndDisabled(); + + if (!hasVanity) + ImGui.EndDisabled(); + }); + } + + private static void DrawSection(string title, float scale, Action body) + { + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(6f, 4f) * scale); + + var flags = ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Framed | ImGuiTreeNodeFlags.AllowItemOverlap | ImGuiTreeNodeFlags.DefaultOpen; + var open = ImGui.CollapsingHeader(title, flags); + ImGui.PopStyleVar(); + + if (open) + { + ImGui.Dummy(new Vector2(0f, 3f * scale)); + body(); + ImGui.Dummy(new Vector2(0f, 2f * scale)); + } + } + + private static bool DrawCheckboxRow(string label, bool currentValue, out bool newValue, string? tooltip = null) + { + bool value = currentValue; + bool changed = UiSharedService.CheckboxWithBorder(label, ref value, UIColors.Get("LightlessPurple"), 1.5f); + if (!string.IsNullOrEmpty(tooltip)) + UiSharedService.AttachToolTip(tooltip); + + newValue = value; + return changed; + } + + private void SyncProfileState(LightlessUserProfileData profile) + { + if (string.Equals(profile.Description, LoadingProfileDescription, StringComparison.Ordinal)) + { + _isNsfwInitialized = false; + return; + } + + if (!_isNsfwInitialized) + { + _userServerIsNsfw = profile.IsNSFW; + _isNsfw = profile.IsNSFW; + _isNsfwInitialized = true; + } + + var profileBytes = profile.ImageData.Value; + if (_pfpTextureWrap == null || !_profileImage.SequenceEqual(profileBytes)) + { + _profileImage = profileBytes; + _pfpTextureWrap?.Dispose(); + _pfpTextureWrap = _profileImage.Length > 0 ? _uiSharedService.LoadImage(_profileImage) : null; + _queuedProfileImage = null; + } + + var bannerBytes = profile.BannerImageData.Value; + if (_bannerTextureWrap == null || !_bannerImage.SequenceEqual(bannerBytes)) + { + _bannerImage = bannerBytes; + _bannerTextureWrap?.Dispose(); + _bannerTextureWrap = _bannerImage.Length > 0 ? _uiSharedService.LoadImage(_bannerImage) : null; + } + + if (!string.Equals(_profileDescription, profile.Description, StringComparison.Ordinal)) + { + _profileDescription = profile.Description; + _descriptionText = _profileDescription; + } + + var serverTags = profile.Tags ?? []; + if (!TagsEqual(serverTags, _profileTagIds)) + { + var previous = _profileTagIds; + _profileTagIds = serverTags.Count == 0 ? [] : [.. serverTags]; + + if (TagsEqual(_tagEditorSelection, previous)) + { + _tagEditorSelection.Clear(); + if (_profileTagIds.Length > 0) + _tagEditorSelection.AddRange(_profileTagIds); + } + } + } + + private static bool IsWindowBeingDragged() + { + return ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows) && ImGui.GetIO().MouseDown[0]; } protected override void Dispose(bool disposing) { base.Dispose(disposing); _pfpTextureWrap?.Dispose(); + _bannerTextureWrap?.Dispose(); } } \ No newline at end of file diff --git a/LightlessSync/UI/EventViewerUI.cs b/LightlessSync/UI/EventViewerUI.cs index 9ce1536..17bcbb2 100644 --- a/LightlessSync/UI/EventViewerUI.cs +++ b/LightlessSync/UI/EventViewerUI.cs @@ -5,6 +5,7 @@ using Dalamud.Interface.Utility.Raii; using LightlessSync.Services; using LightlessSync.Services.Events; using LightlessSync.Services.Mediator; +using LightlessSync.Utils; using Microsoft.Extensions.Logging; using System.Diagnostics; using System.Globalization; @@ -43,11 +44,9 @@ internal class EventViewerUI : WindowMediatorSubscriberBase { _eventAggregator = eventAggregator; _uiSharedService = uiSharedService; - SizeConstraints = new() - { - MinimumSize = new(600, 500), - MaximumSize = new(1000, 2000) - }; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(600, 500), new Vector2(1000, 2000)) + .Apply(); _filteredEvents = RecreateFilter(); } @@ -205,17 +204,37 @@ internal class EventViewerUI : WindowMediatorSubscriberBase var posX = ImGui.GetCursorPosX(); var maxTextLength = ImGui.GetWindowContentRegionMax().X - posX; var textSize = ImGui.CalcTextSize(ev.Message).X; - var msg = ev.Message; - while (textSize > maxTextLength) + var msg = ev.Message ?? string.Empty; + + var maxEventTextLength = ImGui.GetContentRegionAvail().X; + + if (maxEventTextLength <= 0f) { - msg = msg[..^5] + "..."; - textSize = ImGui.CalcTextSize(msg).X; + ImGui.TextUnformatted(string.Empty); + return; } + + var eventTextSize = ImGui.CalcTextSize(msg).X; + + if (eventTextSize > maxEventTextLength) + { + const string ellipsis = "..."; + + while (eventTextSize > maxTextLength && msg.Length > ellipsis.Length) + { + var cut = Math.Min(5, msg.Length - ellipsis.Length); + msg = msg[..^cut] + ellipsis; + eventTextSize = ImGui.CalcTextSize(msg).X; + } + + if (textSize > maxEventTextLength) + msg = ellipsis; + } + ImGui.TextUnformatted(msg); + if (!string.Equals(msg, ev.Message, StringComparison.Ordinal)) - { UiSharedService.AttachToolTip(ev.Message); - } } } } diff --git a/LightlessSync/UI/Handlers/IdDisplayHandler.cs b/LightlessSync/UI/Handlers/IdDisplayHandler.cs index 4d362a9..74a6571 100644 --- a/LightlessSync/UI/Handlers/IdDisplayHandler.cs +++ b/LightlessSync/UI/Handlers/IdDisplayHandler.cs @@ -10,13 +10,16 @@ using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Style; using LightlessSync.Utils; using System; +using System.Collections.Generic; using System.Numerics; +using System.Text; namespace LightlessSync.UI.Handlers; public class IdDisplayHandler { private readonly LightlessConfigService _lightlessConfigService; + private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly LightlessMediator _mediator; private readonly ServerConfigurationManager _serverManager; private readonly Dictionary _showIdForEntry = new(StringComparer.Ordinal); @@ -30,11 +33,16 @@ public class IdDisplayHandler private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f); private float _highlightBoost; - public IdDisplayHandler(LightlessMediator mediator, ServerConfigurationManager serverManager, LightlessConfigService lightlessConfigService) + public IdDisplayHandler( + LightlessMediator mediator, + ServerConfigurationManager serverManager, + LightlessConfigService lightlessConfigService, + PlayerPerformanceConfigService playerPerformanceConfigService) { _mediator = mediator; _serverManager = serverManager; _lightlessConfigService = lightlessConfigService; + _playerPerformanceConfigService = playerPerformanceConfigService; } public void DrawGroupText(string id, GroupFullInfoDto group, float textPosX, Func editBoxWidth) @@ -43,10 +51,18 @@ public class IdDisplayHandler (bool textIsUid, string playerText) = GetGroupText(group); if (!string.Equals(_editEntry, group.GID, StringComparison.Ordinal)) { - ImGui.AlignTextToFramePadding(); - using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid)) + { + ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(playerText); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Left click to switch between ID display and alias" + + Environment.NewLine + "Right click to edit notes for this syncshell" + + Environment.NewLine + "Middle Mouse Button to open syncshell profile in a separate window"); + } if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { @@ -73,6 +89,11 @@ public class IdDisplayHandler _editEntry = group.GID; _editIsUid = false; } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Middle)) + { + _mediator.Publish(new GroupProfileOpenStandaloneMessage(group.Group)); + } } else { @@ -97,112 +118,121 @@ public class IdDisplayHandler { ImGui.SameLine(textPosX); (bool textIsUid, string playerText) = GetPlayerText(pair); + var compactPerformanceText = BuildCompactPerformanceUsageText(pair); if (!string.Equals(_editEntry, pair.UserData.UID, StringComparison.Ordinal)) { - ImGui.AlignTextToFramePadding(); - + var targetFontSize = ImGui.GetFontSize(); var font = textIsUid ? UiBuilder.MonoFont : ImGui.GetFont(); + var rowWidth = MathF.Max(editBoxWidth.Invoke(), 0f); + float rowRightLimit = 0f; + Vector2 nameRectMin = Vector2.Zero; + Vector2 nameRectMax = Vector2.Zero; + float rowTopForStats = 0f; + float frameHeightForStats = 0f; - Vector4? textColor = null; - Vector4? glowColor = null; - - if (pair.UserData.HasVanity) - { - if (!string.IsNullOrWhiteSpace(pair.UserData.TextColorHex)) - { - textColor = UIColors.HexToRgba(pair.UserData.TextColorHex); - } - - if (!string.IsNullOrWhiteSpace(pair.UserData.TextGlowColorHex)) - { - glowColor = UIColors.HexToRgba(pair.UserData.TextGlowColorHex); - } - } - - var useVanityColors = _lightlessConfigService.Current.useColoredUIDs && (textColor != null || glowColor != null); - var seString = useVanityColors - ? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor) - : SeStringUtils.BuildPlain(playerText); - - var rowStart = ImGui.GetCursorScreenPos(); - var drawList = ImGui.GetWindowDrawList(); - bool useHighlight = false; - float highlightPadX = 0f; - float highlightPadY = 0f; - - if (useVanityColors) - { - float boost = Luminance.ComputeHighlight(textColor, glowColor); - - if (boost > 0f) - { - var style = ImGui.GetStyle(); - useHighlight = true; - highlightPadX = MathF.Max(style.FramePadding.X * 0.6f, 2f * ImGuiHelpers.GlobalScale); - highlightPadY = MathF.Max(style.FramePadding.Y * 0.55f, 1.25f * ImGuiHelpers.GlobalScale); - drawList.ChannelsSplit(2); - drawList.ChannelsSetCurrent(1); - - _highlightBoost = boost; - } - else - { - _highlightBoost = 0f; - } - } - - Vector2 itemMin; - Vector2 itemMax; - Vector2 textSize; using (ImRaii.PushFont(font, textIsUid)) { - SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font, pair.UserData.UID); - itemMin = ImGui.GetItemRectMin(); - itemMax = ImGui.GetItemRectMax(); - //textSize = itemMax - itemMin; - } + ImGui.AlignTextToFramePadding(); + var rowStart = ImGui.GetCursorScreenPos(); + rowRightLimit = rowStart.X + rowWidth; - if (useHighlight) + Vector4? textColor = null; + Vector4? glowColor = null; + + if (pair.UserData.HasVanity) + { + if (!string.IsNullOrWhiteSpace(pair.UserData.TextColorHex)) + { + textColor = UIColors.HexToRgba(pair.UserData.TextColorHex); + } + + if (!string.IsNullOrWhiteSpace(pair.UserData.TextGlowColorHex)) + { + glowColor = UIColors.HexToRgba(pair.UserData.TextGlowColorHex); + } + } + + var useVanityColors = _lightlessConfigService.Current.useColoredUIDs && (textColor != null || glowColor != null); + var seString = useVanityColors + ? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor) + : SeStringUtils.BuildPlain(playerText); + + var drawList = ImGui.GetWindowDrawList(); + bool useHighlight = false; + float highlightPadX = 0f; + float highlightPadY = 0f; + + if (useVanityColors) + { + float boost = Luminance.ComputeHighlight(textColor, glowColor); + + if (boost > 0f) + { + var style = ImGui.GetStyle(); + useHighlight = true; + highlightPadX = MathF.Max(style.FramePadding.X * 0.6f, 2f * ImGuiHelpers.GlobalScale); + highlightPadY = MathF.Max(style.FramePadding.Y * 0.55f, 1.25f * ImGuiHelpers.GlobalScale); + drawList.ChannelsSplit(2); + drawList.ChannelsSetCurrent(1); + + _highlightBoost = boost; + } + else + { + _highlightBoost = 0f; + } + } + + SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, targetFontSize, font, pair.UserData.UID); + nameRectMin = ImGui.GetItemRectMin(); + nameRectMax = ImGui.GetItemRectMax(); + + if (useHighlight) + { + var style = ImGui.GetStyle(); + var frameHeight = ImGui.GetFrameHeight(); + var rowTop = rowStart.Y - style.FramePadding.Y; + var rowBottom = rowTop + frameHeight; + + var highlightMin = new Vector2(nameRectMin.X - highlightPadX, rowTop - highlightPadY); + var highlightMax = new Vector2(nameRectMax.X + highlightPadX, rowBottom + highlightPadY); + + var windowPos = ImGui.GetWindowPos(); + var contentMin = windowPos + ImGui.GetWindowContentRegionMin(); + var contentMax = windowPos + ImGui.GetWindowContentRegionMax(); + highlightMin.X = MathF.Max(highlightMin.X, contentMin.X); + highlightMax.X = MathF.Min(highlightMax.X, contentMax.X); + highlightMin.Y = MathF.Max(highlightMin.Y, contentMin.Y); + highlightMax.Y = MathF.Min(highlightMax.Y, contentMax.Y); + + var highlightColor = new Vector4( + 0.25f + _highlightBoost, + 0.25f + _highlightBoost, + 0.25f + _highlightBoost, + 1f + ); + + highlightColor = Luminance.BackgroundContrast(textColor, glowColor, highlightColor, ref _currentBg); + + float rounding = style.FrameRounding > 0f ? style.FrameRounding : 5f * ImGuiHelpers.GlobalScale; + drawList.ChannelsSetCurrent(0); + drawList.AddRectFilled(highlightMin, highlightMax, ImGui.GetColorU32(highlightColor), rounding); + + var borderColor = style.Colors[(int)ImGuiCol.Border]; + borderColor.W *= 0.25f; + drawList.AddRect(highlightMin, highlightMax, ImGui.GetColorU32(borderColor), rounding); + drawList.ChannelsMerge(); + } + } { var style = ImGui.GetStyle(); - var frameHeight = ImGui.GetFrameHeight(); - var rowTop = rowStart.Y - style.FramePadding.Y; - var rowBottom = rowTop + frameHeight; - - var highlightMin = new Vector2(itemMin.X - highlightPadX, rowTop - highlightPadY); - var highlightMax = new Vector2(itemMax.X + highlightPadX, rowBottom + highlightPadY); - - var windowPos = ImGui.GetWindowPos(); - var contentMin = windowPos + ImGui.GetWindowContentRegionMin(); - var contentMax = windowPos + ImGui.GetWindowContentRegionMax(); - highlightMin.X = MathF.Max(highlightMin.X, contentMin.X); - highlightMax.X = MathF.Min(highlightMax.X, contentMax.X); - highlightMin.Y = MathF.Max(highlightMin.Y, contentMin.Y); - highlightMax.Y = MathF.Min(highlightMax.Y, contentMax.Y); - - var highlightColor = new Vector4( - 0.25f + _highlightBoost, - 0.25f + _highlightBoost, - 0.25f + _highlightBoost, - 1f - ); - - highlightColor = Luminance.BackgroundContrast(textColor, glowColor, highlightColor, ref _currentBg); - - float rounding = style.FrameRounding > 0f ? style.FrameRounding : 5f * ImGuiHelpers.GlobalScale; - drawList.ChannelsSetCurrent(0); - drawList.AddRectFilled(highlightMin, highlightMax, ImGui.GetColorU32(highlightColor), rounding); - - var borderColor = style.Colors[(int)ImGuiCol.Border]; - borderColor.W *= 0.25f; - drawList.AddRect(highlightMin, highlightMax, ImGui.GetColorU32(borderColor), rounding); - drawList.ChannelsMerge(); + frameHeightForStats = ImGui.GetFrameHeight(); + rowTopForStats = nameRectMin.Y - style.FramePadding.Y; } - if (ImGui.IsItemHovered()) { - if (!string.Equals(_lastMouseOverUid, id)) + if (!string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal)) { _popupTime = DateTime.UtcNow.AddSeconds(_lightlessConfigService.Current.ProfileDelay); } @@ -223,7 +253,7 @@ public class IdDisplayHandler } else { - if (string.Equals(_lastMouseOverUid, id)) + if (string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal)) { _mediator.Publish(new ProfilePopoutToggle(Pair: null)); _lastMouseOverUid = string.Empty; @@ -261,12 +291,40 @@ public class IdDisplayHandler { _mediator.Publish(new ProfileOpenStandaloneMessage(pair)); } + + if (!string.IsNullOrEmpty(compactPerformanceText)) + { + ImGui.SameLine(); + + const float compactFontScale = 0.85f; + ImGui.SetWindowFontScale(compactFontScale); + var compactHeight = ImGui.GetTextLineHeight(); + var targetPos = ImGui.GetCursorScreenPos(); + var availableWidth = MathF.Max(rowRightLimit - targetPos.X, 0f); + var centeredY = rowTopForStats + MathF.Max((frameHeightForStats - compactHeight) * 0.5f, 0f); + ImGui.SetCursorScreenPos(new Vector2(targetPos.X, centeredY)); + + var performanceText = string.Empty; + var wasTruncated = false; + if (availableWidth > 0f) + { + performanceText = TruncateTextToWidth(compactPerformanceText, availableWidth, out wasTruncated); + } + + ImGui.TextDisabled(performanceText); + ImGui.SetWindowFontScale(1f); + + if (wasTruncated && ImGui.IsItemHovered()) + { + ImGui.SetTooltip(compactPerformanceText); + } + } } else { ImGui.AlignTextToFramePadding(); - ImGui.SetNextItemWidth(editBoxWidth.Invoke()); + ImGui.SetNextItemWidth(MathF.Max(editBoxWidth.Invoke(), 0f)); if (ImGui.InputTextWithHint("##" + pair.UserData.UID, "Nick/Notes", ref _editComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) { _serverManager.SetNoteForUid(pair.UserData.UID, _editComment); @@ -346,6 +404,57 @@ public class IdDisplayHandler return (textIsUid, playerText!); } + private string? BuildCompactPerformanceUsageText(Pair pair) + { + var config = _playerPerformanceConfigService.Current; + if (!config.ShowPerformanceIndicator || !config.ShowPerformanceUsageNextToName) + { + return null; + } + + var vramBytes = pair.LastAppliedApproximateEffectiveVRAMBytes >= 0 + ? pair.LastAppliedApproximateEffectiveVRAMBytes + : pair.LastAppliedApproximateVRAMBytes; + var triangleCount = pair.LastAppliedDataTris; + if (vramBytes < 0 && triangleCount < 0) + { + return null; + } + + var segments = new List(2); + if (vramBytes >= 0) + { + segments.Add(UiSharedService.ByteToString(vramBytes)); + } + + if (triangleCount >= 0) + { + segments.Add(FormatTriangleCount(triangleCount)); + } + + return segments.Count == 0 ? null : string.Join(" / ", segments); + } + + private static string FormatTriangleCount(long triangleCount) + { + if (triangleCount < 0) + { + return string.Empty; + } + + if (triangleCount >= 1_000_000) + { + return FormattableString.Invariant($"{triangleCount / 1_000_000d:0.#}m tris"); + } + + if (triangleCount >= 1_000) + { + return FormattableString.Invariant($"{triangleCount / 1_000d:0.#}k tris"); + } + + return $"{triangleCount} tris"; + } + internal void Clear() { _editEntry = string.Empty; @@ -370,4 +479,52 @@ public class IdDisplayHandler return showidInsteadOfName; } -} \ No newline at end of file + + private static string TruncateTextToWidth(string text, float maxWidth, out bool wasTruncated) + { + wasTruncated = false; + if (string.IsNullOrEmpty(text) || maxWidth <= 0f) + { + return string.Empty; + } + + var fullWidth = ImGui.CalcTextSize(text).X; + if (fullWidth <= maxWidth) + { + return text; + } + + wasTruncated = true; + + const string Ellipsis = "..."; + var ellipsisWidth = ImGui.CalcTextSize(Ellipsis).X; + if (ellipsisWidth >= maxWidth) + { + return Ellipsis; + } + + var builder = new StringBuilder(text.Length); + var remainingWidth = maxWidth - ellipsisWidth; + + foreach (var rune in text.EnumerateRunes()) + { + var runeText = rune.ToString(); + var runeWidth = ImGui.CalcTextSize(runeText).X; + if (runeWidth > remainingWidth) + { + break; + } + + builder.Append(runeText); + remainingWidth -= runeWidth; + } + + if (builder.Length == 0) + { + return Ellipsis; + } + + builder.Append(Ellipsis); + return builder.ToString(); + } +} diff --git a/LightlessSync/UI/IntroUI.cs b/LightlessSync/UI/IntroUI.cs index 470cadb..4fab7ef 100644 --- a/LightlessSync/UI/IntroUI.cs +++ b/LightlessSync/UI/IntroUI.cs @@ -10,6 +10,7 @@ using LightlessSync.Localization; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; +using LightlessSync.Utils; using Microsoft.Extensions.Logging; using System.Numerics; using System.Text.RegularExpressions; @@ -46,11 +47,9 @@ public partial class IntroUi : WindowMediatorSubscriberBase ShowCloseButton = false; RespectCloseHotkey = false; - SizeConstraints = new WindowSizeConstraints() - { - MinimumSize = new Vector2(600, 400), - MaximumSize = new Vector2(600, 2000), - }; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 2000)) + .Apply(); GetToSLocalization(); @@ -267,7 +266,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase { UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed); } - else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey)) + else if (_secretKey.Length == 64 && !SecretRegex().IsMatch(_secretKey)) { UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", ImGuiColors.DalamudRed); } @@ -360,6 +359,6 @@ public partial class IntroUi : WindowMediatorSubscriberBase _tosParagraphs = [Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6]; } - [GeneratedRegex("^([A-F0-9]{2})+")] - private static partial Regex HexRegex(); + [GeneratedRegex("^[A-F0-9]{64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] + private static partial Regex SecretRegex(); } \ No newline at end of file diff --git a/LightlessSync/UI/JoinSyncshellUI.cs b/LightlessSync/UI/JoinSyncshellUI.cs index b02a84e..989fa07 100644 --- a/LightlessSync/UI/JoinSyncshellUI.cs +++ b/LightlessSync/UI/JoinSyncshellUI.cs @@ -1,5 +1,4 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Data.Enum; @@ -174,6 +173,7 @@ internal class JoinSyncshellUI : WindowMediatorSubscriberBase joinPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations); joinPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_groupJoinInfo.Group, _previousPassword, joinPermissions)); + Mediator.Publish(new UserJoinedSyncshell(_groupJoinInfo.Group.GID)); IsOpen = false; } } diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/LightFinderUI.cs similarity index 90% rename from LightlessSync/UI/BroadcastUI.cs rename to LightlessSync/UI/LightFinderUI.cs index c760a45..22911cb 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/LightFinderUI.cs @@ -3,9 +3,11 @@ using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Utility; +using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; using LightlessSync.Services; +using LightlessSync.Services.LightFinder; using LightlessSync.Services.Mediator; using LightlessSync.Utils; using LightlessSync.WebAPI; @@ -14,28 +16,28 @@ using System.Numerics; namespace LightlessSync.UI { - public class BroadcastUI : WindowMediatorSubscriberBase + public class LightFinderUI : WindowMediatorSubscriberBase { private readonly ApiController _apiController; private readonly LightlessConfigService _configService; - private readonly BroadcastService _broadcastService; + private readonly LightFinderService _broadcastService; private readonly UiSharedService _uiSharedService; - private readonly BroadcastScannerService _broadcastScannerService; + private readonly LightFinderScannerService _broadcastScannerService; - private IReadOnlyList _allSyncshells; + private IReadOnlyList _allSyncshells = Array.Empty(); private string _userUid = string.Empty; private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new(); - public BroadcastUI( - ILogger logger, + public LightFinderUI( + ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollectorService, - BroadcastService broadcastService, + LightFinderService broadcastService, LightlessConfigService configService, UiSharedService uiShared, ApiController apiController, - BroadcastScannerService broadcastScannerService + LightFinderScannerService broadcastScannerService ) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService) { _broadcastService = broadcastService; @@ -45,19 +47,17 @@ namespace LightlessSync.UI _broadcastScannerService = broadcastScannerService; IsOpen = false; - this.SizeConstraints = new() - { - MinimumSize = new(600, 465), - MaximumSize = new(750, 525) - }; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(600, 465), new Vector2(750, 525)) + .Apply(); } private void RebuildSyncshellDropdownOptions() { var selectedGid = _configService.Current.SelectedFinderSyncshell; - var allSyncshells = _allSyncshells ?? Array.Empty(); - var ownedSyncshells = allSyncshells - .Where(g => string.Equals(g.OwnerUID, _userUid, StringComparison.Ordinal)) + var allSyncshells = _allSyncshells ?? []; + var filteredSyncshells = allSyncshells + .Where(g => string.Equals(g.OwnerUID, _userUid, StringComparison.Ordinal) || g.GroupUserInfo.IsModerator()) .ToList(); _syncshellOptions.Clear(); @@ -65,7 +65,7 @@ namespace LightlessSync.UI var addedGids = new HashSet(StringComparer.Ordinal); - foreach (var shell in ownedSyncshells) + foreach (var shell in filteredSyncshells) { var label = shell.GroupAliasOrGID ?? shell.GID; _syncshellOptions.Add((label, shell.GID, true)); @@ -143,9 +143,9 @@ namespace LightlessSync.UI _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"),"This lets other Lightless users know you use Lightless. While enabled, you and others using Lightfinder can see each other identified as Lightless users."); ImGui.Indent(15f); - ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); - ImGui.Text("- This is done using a 'Lightless' label above player nameplates."); - ImGui.PopStyleColor(); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); + ImGui.Text("- This is done using a 'Lightless' label above player nameplates."); + ImGui.PopStyleColor(); ImGui.Unindent(15f); ImGuiHelpers.ScaledDummy(3f); @@ -192,7 +192,7 @@ namespace LightlessSync.UI ImGui.PopStyleVar(); ImGuiHelpers.ScaledDummy(3f); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); if (_configService.Current.BroadcastEnabled) { @@ -288,7 +288,7 @@ namespace LightlessSync.UI _uiSharedService.MediumText("Syncshell Finder", UIColors.Get("PairBlue")); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); ImGui.PushTextWrapPos(); ImGui.Text("Allow your owned Syncshell to be indexed by the Nearby Syncshell Finder."); @@ -296,11 +296,19 @@ namespace LightlessSync.UI ImGui.PopTextWrapPos(); ImGuiHelpers.ScaledDummy(0.2f); - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled; bool isBroadcasting = _broadcastService.IsBroadcasting; + if (isBroadcasting) + { + var warningColor = UIColors.Get("LightlessYellow"); + _uiSharedService.DrawNoteLine("! ", warningColor, + new SeStringUtils.RichTextEntry("Syncshell Finder can only be changed while Lightfinder is disabled.", warningColor)); + ImGuiHelpers.ScaledDummy(0.2f); + } + if (isBroadcasting) ImGui.BeginDisabled(); @@ -369,7 +377,7 @@ namespace LightlessSync.UI ImGui.EndTabItem(); } - #if DEBUG +#if DEBUG if (ImGui.BeginTabItem("Debug")) { ImGui.Text("Broadcast Cache"); @@ -428,7 +436,7 @@ namespace LightlessSync.UI ImGui.EndTabItem(); } - #endif +#endif ImGui.EndTabBar(); } diff --git a/LightlessSync/UI/LightlessNotificationUI.cs b/LightlessSync/UI/LightlessNotificationUI.cs index 8cb6922..1d0a477 100644 --- a/LightlessSync/UI/LightlessNotificationUI.cs +++ b/LightlessSync/UI/LightlessNotificationUI.cs @@ -1,13 +1,12 @@ using Dalamud.Interface; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using Dalamud.Interface.Windowing; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.UI.Models; +using LightlessSync.UI.Style; using Microsoft.Extensions.Logging; using System.Numerics; @@ -27,11 +26,12 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase private const float _titleMessageSpacing = 4f; private const float _actionButtonSpacing = 8f; - private readonly List _notifications = new(); + private readonly List _notifications = []; private readonly object _notificationLock = new(); private readonly LightlessConfigService _configService; - private readonly Dictionary _notificationYOffsets = new(); - private readonly Dictionary _notificationTargetYOffsets = new(); + private readonly Dictionary _notificationYOffsets = []; + private readonly Dictionary _notificationTargetYOffsets = []; + private readonly Dictionary _notificationBackgrounds = []; public LightlessNotificationUi(ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService) : base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector) @@ -45,7 +45,6 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoCollapse | - ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.AlwaysAutoResize; @@ -68,7 +67,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase { lock (_notificationLock) { - var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id); + var existingNotification = _notifications.FirstOrDefault(n => string.Equals(n.Id, notification.Id, StringComparison.Ordinal)); if (existingNotification != null) { UpdateExistingNotification(existingNotification, notification); @@ -76,7 +75,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase else { _notifications.Add(notification); - _logger.LogDebug("Added new notification: {Title}", notification.Title); + _logger.LogTrace("Added new notification: {Title}", notification.Title); } if (!IsOpen) IsOpen = true; @@ -96,14 +95,14 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase existing.CreatedAt = DateTime.UtcNow; } - _logger.LogDebug("Updated existing notification: {Title}", updated.Title); + _logger.LogTrace("Updated existing notification: {Title}", updated.Title); } public void RemoveNotification(string id) { lock (_notificationLock) { - var notification = _notifications.FirstOrDefault(n => n.Id == id); + var notification = _notifications.FirstOrDefault(n => string.Equals(n.Id, id, StringComparison.Ordinal)); if (notification != null) { StartOutAnimation(notification); @@ -122,13 +121,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private void StartOutAnimation(LightlessNotification notification) + private static void StartOutAnimation(LightlessNotification notification) { notification.IsAnimatingOut = true; notification.IsAnimatingIn = false; } - private bool ShouldRemoveNotification(LightlessNotification notification) => + private static bool ShouldRemoveNotification(LightlessNotification notification) => notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f; protected override void DrawInternal() @@ -162,30 +161,30 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase { var corner = _configService.Current.NotificationCorner; var offsetX = _configService.Current.NotificationOffsetX; + var offsetY = _configService.Current.NotificationOffsetY; var width = _configService.Current.NotificationWidth; - + float posX = corner == NotificationCorner.Left ? viewport.WorkPos.X + offsetX - _windowPaddingOffset : viewport.WorkPos.X + viewport.WorkSize.X - width - offsetX - _windowPaddingOffset; - - return new Vector2(posX, viewport.WorkPos.Y); + + float posY = viewport.WorkPos.Y + offsetY; + + return new Vector2(posX, posY); } private void DrawAllNotifications() { - var offsetY = _configService.Current.NotificationOffsetY; - var startY = ImGui.GetCursorPosY() + offsetY; - + var startY = ImGui.GetCursorPosY(); + for (int i = 0; i < _notifications.Count; i++) { var notification = _notifications[i]; - + if (_notificationYOffsets.TryGetValue(notification.Id, out var yOffset)) - { ImGui.SetCursorPosY(startY + yOffset); - } - - DrawNotification(notification, i); + + DrawNotification(notification); } } @@ -228,6 +227,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase _notifications.RemoveAt(i); _notificationYOffsets.Remove(notification.Id); _notificationTargetYOffsets.Remove(notification.Id); + _notificationBackgrounds.Remove(notification.Id); } } } @@ -304,7 +304,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase return corner == NotificationCorner.Left ? new Vector2(-distance, 0) : new Vector2(distance, 0); } - private void DrawNotification(LightlessNotification notification, int index) + private void DrawNotification(LightlessNotification notification) { var alpha = notification.AnimationProgress; if (alpha <= 0f) return; @@ -336,14 +336,15 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); - var bgColor = CalculateBackgroundColor(alpha, ImGui.IsWindowHovered()); var accentColor = GetNotificationAccentColor(notification.Type); - accentColor.W *= alpha; - + var bgColor = CalculateBackgroundColor(notification, alpha, ImGui.IsWindowHovered(), accentColor); + var accentColorWithAlpha = accentColor; + accentColorWithAlpha.W *= alpha; + DrawShadow(drawList, windowPos, windowSize, alpha); HandleClickToDismiss(notification); DrawBackground(drawList, windowPos, windowSize, bgColor); - DrawAccentBar(drawList, windowPos, windowSize, accentColor); + DrawAccentBar(drawList, windowPos, windowSize, accentColorWithAlpha); DrawDurationProgressBar(notification, alpha, windowPos, windowSize, drawList); // Draw download progress bar above duration bar for download notifications @@ -355,22 +356,44 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase DrawNotificationText(notification, alpha); } - private Vector4 CalculateBackgroundColor(float alpha, bool isHovered) + private Vector4 CalculateBackgroundColor(LightlessNotification notification, float alpha, bool isHovered, Vector4 accentColor) { var baseOpacity = _configService.Current.NotificationOpacity; var finalOpacity = baseOpacity * alpha; - var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, finalOpacity); + float boost = Luminance.ComputeHighlight(null, accentColor); + + var baseBg = new Vector4( + 30f/255f + boost, + 30f/255f + boost, + 30f/255f + boost, + finalOpacity + ); + + if (!_notificationBackgrounds.ContainsKey(notification.Id)) + { + _notificationBackgrounds[notification.Id] = baseBg; + } + + var currentBg = _notificationBackgrounds[notification.Id]; + var bgColor = Luminance.BackgroundContrast(null, accentColor, baseBg, ref currentBg); + _notificationBackgrounds[notification.Id] = currentBg; + + bgColor.W = finalOpacity; if (isHovered) { - bgColor *= 1.1f; - bgColor.W = Math.Min(bgColor.W, 0.98f); + bgColor = new Vector4( + bgColor.X * 1.1f, + bgColor.Y * 1.1f, + bgColor.Z * 1.1f, + Math.Min(bgColor.W, 0.98f) + ); } return bgColor; } - private void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha) + private static void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha) { var shadowOffset = new Vector2(1f, 1f); var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha); @@ -384,9 +407,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase private void HandleClickToDismiss(LightlessNotification notification) { - if (ImGui.IsWindowHovered() && + var pos = ImGui.GetWindowPos(); + var size = ImGui.GetWindowSize(); + bool hovered = ImGui.IsMouseHoveringRect(pos, new Vector2(pos.X + size.X, pos.Y + size.Y)); + + if ((hovered || ImGui.IsWindowHovered()) && _configService.Current.DismissNotificationOnClick && - !notification.Actions.Any() && + notification.Actions.Count == 0 && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) { notification.IsDismissed = true; @@ -394,7 +421,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor) + private static void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor) { drawList.AddRectFilled( windowPos, @@ -431,14 +458,14 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase ); } - private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) + private static void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) { var progress = CalculateDurationProgress(notification); var progressBarColor = UIColors.Get("LightlessBlue"); var progressHeight = 2f; var progressY = windowPos.Y + windowSize.Y - progressHeight; var progressWidth = windowSize.X * progress; - + DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha); if (progress > 0) @@ -447,7 +474,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private void DrawDownloadProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) + private static void DrawDownloadProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList) { var progress = Math.Clamp(notification.Progress, 0f, 1f); var progressBarColor = UIColors.Get("LightlessGreen"); @@ -455,7 +482,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase // Position above the duration bar (2px duration bar + 1px spacing) var progressY = windowPos.Y + windowSize.Y - progressHeight - 3f; var progressWidth = windowSize.X * progress; - + DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha); if (progress > 0) @@ -464,14 +491,14 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private float CalculateDurationProgress(LightlessNotification notification) + private static float CalculateDurationProgress(LightlessNotification notification) { // Calculate duration timer progress var elapsed = DateTime.UtcNow - notification.CreatedAt; return Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds)); } - private void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha) + private static void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha) { var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha); drawList.AddRectFilled( @@ -482,7 +509,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase ); } - private void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha) + private static void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha) { var progressColor = progressBarColor; progressColor.W *= alpha; @@ -512,13 +539,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private float CalculateContentWidth(float windowWidth) => + private static float CalculateContentWidth(float windowWidth) => windowWidth - (_contentPaddingX * 2); - private bool HasActions(LightlessNotification notification) => + private static bool HasActions(LightlessNotification notification) => notification.Actions.Count > 0; - private void PositionActionsAtBottom(float windowHeight) + private static void PositionActionsAtBottom(float windowHeight) { var actionHeight = ImGui.GetFrameHeight(); var bottomY = windowHeight - _contentPaddingY - actionHeight; @@ -546,7 +573,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase return $"[{timestamp}] {notification.Title}"; } - private float DrawWrappedText(string text, float wrapWidth) + private static float DrawWrappedText(string text, float wrapWidth) { ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + wrapWidth); var startY = ImGui.GetCursorPosY(); @@ -556,7 +583,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase return height; } - private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha) + private static void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha) { if (string.IsNullOrEmpty(notification.Message)) return; @@ -575,7 +602,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase { var buttonWidth = CalculateActionButtonWidth(notification.Actions.Count, availableWidth); - _logger.LogDebug("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}", + _logger.LogTrace("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}", notification.Actions.Count, buttonWidth, availableWidth); var startX = ImGui.GetCursorPosX(); @@ -591,13 +618,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private float CalculateActionButtonWidth(int actionCount, float availableWidth) + private static float CalculateActionButtonWidth(int actionCount, float availableWidth) { var totalSpacing = (actionCount - 1) * _actionButtonSpacing; return (availableWidth - totalSpacing) / actionCount; } - private void PositionActionButton(int index, float startX, float buttonWidth) + private static void PositionActionButton(int index, float startX, float buttonWidth) { var xPosition = startX + index * (buttonWidth + _actionButtonSpacing); ImGui.SetCursorPosX(xPosition); @@ -605,7 +632,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase private void DrawActionButton(LightlessNotificationAction action, LightlessNotification notification, float alpha, float buttonWidth) { - _logger.LogDebug("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth); + _logger.LogTrace("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth); var buttonColor = action.Color; buttonColor.W *= alpha; @@ -625,22 +652,22 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase if (action.Icon != FontAwesomeIcon.None) { - buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth, alpha); + buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth); } else { buttonPressed = ImGui.Button(action.Label, new Vector2(buttonWidth, 0)); } - _logger.LogDebug("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed); + _logger.LogTrace("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed); if (buttonPressed) { try { - _logger.LogDebug("Executing action: {ActionId}", action.Id); + _logger.LogTrace("Executing action: {ActionId}", action.Id); action.OnClick(notification); - _logger.LogDebug("Action executed successfully: {ActionId}", action.Id); + _logger.LogTrace("Action executed successfully: {ActionId}", action.Id); } catch (Exception ex) { @@ -650,10 +677,10 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase } } - private bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width, float alpha) + private static bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width) { var drawList = ImGui.GetWindowDrawList(); - var cursorPos = ImGui.GetCursorScreenPos(); + ImGui.GetCursorScreenPos(); var frameHeight = ImGui.GetFrameHeight(); Vector2 iconSize; @@ -729,7 +756,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase return ImGui.CalcTextSize(titleText, true, contentWidth).Y; } - private float CalculateMessageHeight(LightlessNotification notification, float contentWidth) + private static float CalculateMessageHeight(LightlessNotification notification, float contentWidth) { if (string.IsNullOrEmpty(notification.Message)) return 0f; @@ -737,7 +764,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase return 4f + messageHeight; } - private Vector4 GetNotificationAccentColor(NotificationType type) + private static Vector4 GetNotificationAccentColor(NotificationType type) { return type switch { diff --git a/LightlessSync/UI/Models/Changelog.cs b/LightlessSync/UI/Models/Changelog.cs deleted file mode 100644 index 23d26c4..0000000 --- a/LightlessSync/UI/Models/Changelog.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace LightlessSync.UI.Models -{ - public class ChangelogFile - { - public string Tagline { get; init; } = string.Empty; - public string Subline { get; init; } = string.Empty; - public List Changelog { get; init; } = new(); - public List? Credits { get; init; } - } - - public class ChangelogEntry - { - public string Name { get; init; } = string.Empty; - public string Date { get; init; } = string.Empty; - public string Tagline { get; init; } = string.Empty; - public bool? IsCurrent { get; init; } - public string? Message { get; init; } - public List? Versions { get; init; } - } - - public class ChangelogVersion - { - public string Number { get; init; } = string.Empty; - public List Items { get; init; } = new(); - } - - public class CreditCategory - { - public string Category { get; init; } = string.Empty; - public List Items { get; init; } = new(); - } - - public class CreditItem - { - public string Name { get; init; } = string.Empty; - public string Role { get; init; } = string.Empty; - } - - public class CreditsFile - { - public List Credits { get; init; } = new(); - } -} \ No newline at end of file diff --git a/LightlessSync/UI/Models/ChangelogEntry.cs b/LightlessSync/UI/Models/ChangelogEntry.cs new file mode 100644 index 0000000..919a6da --- /dev/null +++ b/LightlessSync/UI/Models/ChangelogEntry.cs @@ -0,0 +1,12 @@ +namespace LightlessSync.UI.Models +{ + public class ChangelogEntry + { + public string Name { get; init; } = string.Empty; + public string Date { get; init; } = string.Empty; + public string Tagline { get; init; } = string.Empty; + public bool? IsCurrent { get; init; } + public string? Message { get; init; } + public List? Versions { get; init; } + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/ChangelogFile.cs b/LightlessSync/UI/Models/ChangelogFile.cs new file mode 100644 index 0000000..37997c8 --- /dev/null +++ b/LightlessSync/UI/Models/ChangelogFile.cs @@ -0,0 +1,10 @@ +namespace LightlessSync.UI.Models +{ + public class ChangelogFile + { + public string Tagline { get; init; } = string.Empty; + public string Subline { get; init; } = string.Empty; + public List Changelog { get; init; } = new(); + public List? Credits { get; init; } + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/ChangelogVersion.cs b/LightlessSync/UI/Models/ChangelogVersion.cs new file mode 100644 index 0000000..b70ace6 --- /dev/null +++ b/LightlessSync/UI/Models/ChangelogVersion.cs @@ -0,0 +1,8 @@ +namespace LightlessSync.UI.Models +{ + public class ChangelogVersion + { + public string Number { get; init; } = string.Empty; + public List Items { get; init; } = []; + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/CreditCategory.cs b/LightlessSync/UI/Models/CreditCategory.cs new file mode 100644 index 0000000..5b25cca --- /dev/null +++ b/LightlessSync/UI/Models/CreditCategory.cs @@ -0,0 +1,8 @@ +namespace LightlessSync.UI.Models +{ + public class CreditCategory + { + public string Category { get; init; } = string.Empty; + public List Items { get; init; } = []; + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/CreditItem.cs b/LightlessSync/UI/Models/CreditItem.cs new file mode 100644 index 0000000..ae0c4be --- /dev/null +++ b/LightlessSync/UI/Models/CreditItem.cs @@ -0,0 +1,8 @@ +namespace LightlessSync.UI.Models +{ + public class CreditItem + { + public string Name { get; init; } = string.Empty; + public string Role { get; init; } = string.Empty; + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/CreditsFile.cs b/LightlessSync/UI/Models/CreditsFile.cs new file mode 100644 index 0000000..b6b6a83 --- /dev/null +++ b/LightlessSync/UI/Models/CreditsFile.cs @@ -0,0 +1,7 @@ +namespace LightlessSync.UI.Models +{ + public class CreditsFile + { + public List Credits { get; init; } = []; + } +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/LightlessNotification.cs b/LightlessSync/UI/Models/LightlessNotification.cs index 3c6edea..4ae49a4 100644 --- a/LightlessSync/UI/Models/LightlessNotification.cs +++ b/LightlessSync/UI/Models/LightlessNotification.cs @@ -1,7 +1,7 @@ -using Dalamud.Interface; using LightlessSync.LightlessConfiguration.Models; -using System.Numerics; + namespace LightlessSync.UI.Models; + public class LightlessNotification { public string Id { get; set; } = Guid.NewGuid().ToString(); @@ -20,13 +20,3 @@ public class LightlessNotification public bool IsAnimatingOut { get; set; } = false; public uint? SoundEffectId { get; set; } = null; } -public class LightlessNotificationAction -{ - public string Id { get; set; } = Guid.NewGuid().ToString(); - public string Label { get; set; } = string.Empty; - public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None; - public Vector4 Color { get; set; } = Vector4.One; - public Action OnClick { get; set; } = _ => { }; - public bool IsPrimary { get; set; } = false; - public bool IsDestructive { get; set; } = false; -} \ No newline at end of file diff --git a/LightlessSync/UI/Models/LightlessNotificationAction.cs b/LightlessSync/UI/Models/LightlessNotificationAction.cs new file mode 100644 index 0000000..7c9fd53 --- /dev/null +++ b/LightlessSync/UI/Models/LightlessNotificationAction.cs @@ -0,0 +1,15 @@ +using Dalamud.Interface; +using System.Numerics; + +namespace LightlessSync.UI.Models; + +public class LightlessNotificationAction +{ + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string Label { get; set; } = string.Empty; + public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None; + public Vector4 Color { get; set; } = Vector4.One; + public Action OnClick { get; set; } = _ => { }; + public bool IsPrimary { get; set; } = false; + public bool IsDestructive { get; set; } = false; +} \ No newline at end of file diff --git a/LightlessSync/UI/Models/OnlinePairSortMode.cs b/LightlessSync/UI/Models/OnlinePairSortMode.cs new file mode 100644 index 0000000..ff85b9c --- /dev/null +++ b/LightlessSync/UI/Models/OnlinePairSortMode.cs @@ -0,0 +1,7 @@ +namespace LightlessSync.UI.Models; + +public enum OnlinePairSortMode +{ + Alphabetical = 0, + PreferredDirectPairs = 1, +} diff --git a/LightlessSync/UI/Models/PairDisplayEntry.cs b/LightlessSync/UI/Models/PairDisplayEntry.cs new file mode 100644 index 0000000..e89e7fe --- /dev/null +++ b/LightlessSync/UI/Models/PairDisplayEntry.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Dto.Group; +using LightlessSync.PlayerData.Pairs; + +namespace LightlessSync.UI.Models; + +public sealed record PairDisplayEntry( + PairUniqueIdentifier Ident, + PairConnection Connection, + IReadOnlyList Groups, + IPairHandlerAdapter? Handler) +{ + public UserData User => Connection.User; + public bool IsOnline => Connection.IsOnline; + public bool IsVisible => Handler?.IsVisible ?? false; + public bool IsDirectlyPaired => Connection.IsDirectlyPaired; + public bool IsOneSided => Connection.IsOneSided; + public bool HasAnyConnection => Connection.HasAnyConnection; + public string? IdentString => Connection.Ident; + public UserPermissions SelfPermissions => Connection.SelfToOtherPermissions; + public UserPermissions OtherPermissions => Connection.OtherToSelfPermissions; + public IndividualPairStatus? PairStatus => Connection.IndividualPairStatus; +} diff --git a/LightlessSync/UI/Models/PairUiEntry.cs b/LightlessSync/UI/Models/PairUiEntry.cs new file mode 100644 index 0000000..c25b6fd --- /dev/null +++ b/LightlessSync/UI/Models/PairUiEntry.cs @@ -0,0 +1,30 @@ +using LightlessSync.API.Data; +using LightlessSync.API.Data.Enum; +using LightlessSync.API.Dto.Group; +using LightlessSync.PlayerData.Pairs; + +namespace LightlessSync.UI.Models; + +public sealed record PairUiEntry( + PairDisplayEntry DisplayEntry, + string AliasOrUid, + string DisplayName, + string Note, + bool IsVisible, + bool IsOnline, + bool IsDirectlyPaired, + bool IsOneSided, + bool HasAnyConnection, + bool IsPaused, + UserPermissions SelfPermissions, + UserPermissions OtherPermissions, + IndividualPairStatus? PairStatus, + long LastAppliedDataBytes, + long LastAppliedDataTris, + long LastAppliedApproximateVramBytes, + long LastAppliedApproximateEffectiveVramBytes, + IPairHandlerAdapter? Handler) +{ + public PairUniqueIdentifier Ident => DisplayEntry.Ident; + public IReadOnlyList Groups => DisplayEntry.Groups; +} diff --git a/LightlessSync/UI/Models/PairUiSnapshot.cs b/LightlessSync/UI/Models/PairUiSnapshot.cs new file mode 100644 index 0000000..c9226b0 --- /dev/null +++ b/LightlessSync/UI/Models/PairUiSnapshot.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using LightlessSync.API.Dto.Group; +using LightlessSync.PlayerData.Pairs; + +namespace LightlessSync.UI.Models; + +public sealed record PairUiSnapshot( + IReadOnlyDictionary PairsByUid, + IReadOnlyList DirectPairs, + IReadOnlyDictionary> GroupPairs, + IReadOnlyDictionary> PairsWithGroups, + IReadOnlyDictionary GroupsByGid, + IReadOnlyCollection Groups) +{ + public static PairUiSnapshot Empty { get; } = new( + new ReadOnlyDictionary(new Dictionary()), + Array.Empty(), + new ReadOnlyDictionary>(new Dictionary>()), + new ReadOnlyDictionary>(new Dictionary>()), + new ReadOnlyDictionary(new Dictionary()), + Array.Empty()); +} diff --git a/LightlessSync/UI/Models/VisiblePairSortMode.cs b/LightlessSync/UI/Models/VisiblePairSortMode.cs new file mode 100644 index 0000000..ec133b9 --- /dev/null +++ b/LightlessSync/UI/Models/VisiblePairSortMode.cs @@ -0,0 +1,10 @@ +namespace LightlessSync.UI.Models; + +public enum VisiblePairSortMode +{ + Alphabetical = 0, + VramUsage = 1, + EffectiveVramUsage = 2, + TriangleCount = 3, + PreferredDirectPairs = 4, +} diff --git a/LightlessSync/UI/PermissionWindowUI.cs b/LightlessSync/UI/PermissionWindowUI.cs index 45be154..5dee098 100644 --- a/LightlessSync/UI/PermissionWindowUI.cs +++ b/LightlessSync/UI/PermissionWindowUI.cs @@ -9,6 +9,7 @@ using LightlessSync.Services.Mediator; using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; +using System.Numerics; namespace LightlessSync.UI; @@ -28,12 +29,10 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase _uiSharedService = uiSharedService; _apiController = apiController; _ownPermissions = pair.UserPair.OwnPermissions.DeepClone(); - Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize; - SizeConstraints = new() - { - MinimumSize = new(450, 100), - MaximumSize = new(450, 500) - }; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(450, 100), new Vector2(450, 500)) + .AddFlags(ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize) + .Apply(); IsOpen = true; } diff --git a/LightlessSync/UI/PopoutProfileUi.cs b/LightlessSync/UI/PopoutProfileUi.cs index 1737214..baa41a2 100644 --- a/LightlessSync/UI/PopoutProfileUi.cs +++ b/LightlessSync/UI/PopoutProfileUi.cs @@ -4,10 +4,11 @@ using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using LightlessSync.API.Data.Extensions; using LightlessSync.LightlessConfiguration; -using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; +using LightlessSync.UI.Services; +using LightlessSync.PlayerData.Pairs; using Microsoft.Extensions.Logging; using System.Numerics; @@ -16,7 +17,7 @@ namespace LightlessSync.UI; public class PopoutProfileUi : WindowMediatorSubscriberBase { private readonly LightlessProfileManager _lightlessProfileManager; - private readonly PairManager _pairManager; + private readonly PairUiService _pairUiService; private readonly ServerConfigurationManager _serverManager; private readonly UiSharedService _uiSharedService; private Vector2 _lastMainPos = Vector2.Zero; @@ -29,12 +30,12 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase public PopoutProfileUi(ILogger logger, LightlessMediator mediator, UiSharedService uiBuilder, ServerConfigurationManager serverManager, LightlessConfigService lightlessConfigService, - LightlessProfileManager lightlessProfileManager, PairManager pairManager, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "###LightlessSyncPopoutProfileUI", performanceCollectorService) + LightlessProfileManager lightlessProfileManager, PairUiService pairUiService, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "###LightlessSyncPopoutProfileUI", performanceCollectorService) { _uiSharedService = uiBuilder; _serverManager = serverManager; _lightlessProfileManager = lightlessProfileManager; - _pairManager = pairManager; + _pairUiService = pairUiService; Flags = ImGuiWindowFlags.NoDecoration; Mediator.Subscribe(this, (msg) => @@ -143,13 +144,17 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase UiSharedService.ColorText("They: paused", UIColors.Get("LightlessYellow")); } } + var snapshot = _pairUiService.GetSnapshot(); + if (_pair.UserPair.Groups.Any()) { ImGui.TextUnformatted("Paired through Syncshells:"); foreach (var group in _pair.UserPair.Groups) { var groupNote = _serverManager.GetNoteForGid(group); - var groupName = _pairManager.GroupPairs.First(f => string.Equals(f.Key.GID, group, StringComparison.Ordinal)).Key.GroupAliasOrGID; + var groupName = snapshot.GroupsByGid.TryGetValue(group, out var groupInfo) + ? groupInfo.GroupAliasOrGID + : group; var groupString = string.IsNullOrEmpty(groupNote) ? groupName : $"{groupNote} ({groupName})"; ImGui.TextUnformatted("- " + groupString); } diff --git a/LightlessSync/UI/ProfileEditorLayoutCoordinator.cs b/LightlessSync/UI/ProfileEditorLayoutCoordinator.cs new file mode 100644 index 0000000..9a980fe --- /dev/null +++ b/LightlessSync/UI/ProfileEditorLayoutCoordinator.cs @@ -0,0 +1,84 @@ +using System; +using System.Numerics; +using System.Threading; + +namespace LightlessSync.UI; + +internal static class ProfileEditorLayoutCoordinator +{ + private static readonly Lock Gate = new(); + private static string? _activeUid; + private static Vector2? _anchor; + + private const float ProfileWidth = 840f; + private const float ProfileHeight = 525f; + private const float EditorWidth = 380f; + private const float Spacing = 0f; + private static readonly Vector2 DefaultOffset = new(50f, 70f); + + public static void Enable(string uid) + { + using var _ = Gate.EnterScope(); + if (!string.Equals(_activeUid, uid, StringComparison.Ordinal)) + _anchor = null; + _activeUid = uid; + } + + public static void Disable(string uid) + { + using var _ = Gate.EnterScope(); + if (string.Equals(_activeUid, uid, StringComparison.Ordinal)) + { + _activeUid = null; + _anchor = null; + } + } + + public static bool IsActive(string uid) + { + using var _ = Gate.EnterScope(); + return string.Equals(_activeUid, uid, StringComparison.Ordinal); + } + + public static Vector2 GetProfileSize(float scale) => new(ProfileWidth * scale, ProfileHeight * scale); + public static Vector2 GetEditorSize(float scale) => new(EditorWidth * scale, ProfileHeight * scale); + + public static Vector2 GetEditorOffset(float scale) => new((ProfileWidth + Spacing) * scale, 0f); + + public static Vector2 EnsureAnchor(Vector2 viewportOrigin, float scale) + { + using var _ = Gate.EnterScope(); + if (_anchor is null) + _anchor = viewportOrigin + DefaultOffset * scale; + return _anchor.Value; + } + + public static void UpdateAnchorFromProfile(Vector2 profilePosition) + { + using var _ = Gate.EnterScope(); + _anchor = profilePosition; + } + + public static void UpdateAnchorFromEditor(Vector2 editorPosition, float scale) + { + using var _ = Gate.EnterScope(); + _anchor = editorPosition - GetEditorOffset(scale); + } + + public static Vector2 GetProfilePosition(float scale) + { + using var _ = Gate.EnterScope(); + return _anchor ?? Vector2.Zero; + } + + public static Vector2 GetEditorPosition(float scale) + { + using var _ = Gate.EnterScope(); + return (_anchor ?? Vector2.Zero) + GetEditorOffset(scale); + } + + public static bool NearlyEquals(Vector2 current, Vector2 target, float epsilon = 0.5f) + { + return MathF.Abs(current.X - target.X) <= epsilon && MathF.Abs(current.Y - target.Y) <= epsilon; + } +} diff --git a/LightlessSync/UI/ProfileTags.cs b/LightlessSync/UI/ProfileTags.cs deleted file mode 100644 index 9fe4d6c..0000000 --- a/LightlessSync/UI/ProfileTags.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace LightlessSync.UI -{ - public enum ProfileTags - { - SFW = 0, - NSFW = 1, - RP = 2, - ERP = 3, - Venues = 4, - Gpose = 5 - } -} \ No newline at end of file diff --git a/LightlessSync/UI/Services/PairUiService.cs b/LightlessSync/UI/Services/PairUiService.cs new file mode 100644 index 0000000..290a7fb --- /dev/null +++ b/LightlessSync/UI/Services/PairUiService.cs @@ -0,0 +1,225 @@ +using System.Collections.ObjectModel; +using LightlessSync.API.Dto.Group; +using LightlessSync.PlayerData.Factories; +using LightlessSync.PlayerData.Pairs; +using LightlessSync.Services.Mediator; +using LightlessSync.UI.Models; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.UI.Services; + +public sealed class PairUiService : DisposableMediatorSubscriberBase +{ + private readonly PairLedger _pairLedger; + private readonly PairFactory _pairFactory; + private readonly PairManager _pairManager; + + private readonly object _snapshotGate = new(); + private PairUiSnapshot _snapshot = PairUiSnapshot.Empty; + private Pair? _lastAddedPair; + private bool _needsRefresh = true; + + public PairUiService( + ILogger logger, + LightlessMediator mediator, + PairLedger pairLedger, + PairFactory pairFactory, + PairManager pairManager) : base(logger, mediator) + { + _pairLedger = pairLedger; + _pairFactory = pairFactory; + _pairManager = pairManager; + + Mediator.Subscribe(this, _ => MarkDirty()); + Mediator.Subscribe(this, _ => MarkDirty()); + Mediator.Subscribe(this, _ => MarkDirty()); + + EnsureSnapshot(); + } + + public PairUiSnapshot GetSnapshot() + { + EnsureSnapshot(); + lock (_snapshotGate) + { + return _snapshot; + } + } + + public Pair? GetLastAddedPair() + { + EnsureSnapshot(); + lock (_snapshotGate) + { + return _lastAddedPair; + } + } + + public void ClearLastAddedPair() + { + _pairManager.ClearLastAddedUser(); + lock (_snapshotGate) + { + _lastAddedPair = null; + } + } + + private void MarkDirty() + { + lock (_snapshotGate) + { + _needsRefresh = true; + } + } + + private void EnsureSnapshot() + { + bool shouldBuild; + lock (_snapshotGate) + { + shouldBuild = _needsRefresh; + if (shouldBuild) + { + _needsRefresh = false; + } + } + + if (!shouldBuild) + { + return; + } + + PairUiSnapshot snapshot; + Pair? lastAddedPair; + try + { + (snapshot, lastAddedPair) = BuildSnapshot(); + } + catch + { + lock (_snapshotGate) + { + _needsRefresh = true; + } + + throw; + } + + lock (_snapshotGate) + { + _snapshot = snapshot; + _lastAddedPair = lastAddedPair; + } + + Mediator.Publish(new PairUiUpdatedMessage(snapshot)); + } + + private (PairUiSnapshot Snapshot, Pair? LastAddedPair) BuildSnapshot() + { + var entries = _pairLedger.GetAllEntries(); + var pairByUid = new Dictionary(StringComparer.Ordinal); + + var directPairsList = new List(); + var groupPairsTemp = new Dictionary>(); + var pairsWithGroupsTemp = new Dictionary>(); + + foreach (var entry in entries) + { + var pair = _pairFactory.Create(entry); + if (pair is null) + { + continue; + } + + pairByUid[entry.Ident.UserId] = pair; + + if (entry.IsDirectlyPaired) + { + directPairsList.Add(pair); + } + + var uniqueGroups = new HashSet(StringComparer.Ordinal); + var groupList = new List(); + foreach (var group in entry.Groups) + { + if (!uniqueGroups.Add(group.Group.GID)) + { + continue; + } + + if (!groupPairsTemp.TryGetValue(group, out var members)) + { + members = new List(); + groupPairsTemp[group] = members; + } + + members.Add(pair); + groupList.Add(group); + } + + pairsWithGroupsTemp[pair] = groupList; + } + + var allGroupsList = _pairLedger.GetAllSyncshells() + .Values + .Select(s => s.GroupFullInfo) + .ToList(); + + foreach (var group in allGroupsList) + { + if (!groupPairsTemp.ContainsKey(group)) + { + groupPairsTemp[group] = new List(); + } + } + + var directPairs = new ReadOnlyCollection(directPairsList); + + var groupPairsFinal = new Dictionary>(); + foreach (var (group, members) in groupPairsTemp) + { + groupPairsFinal[group] = new ReadOnlyCollection(members); + } + + var pairsWithGroupsFinal = new Dictionary>(); + foreach (var (pair, groups) in pairsWithGroupsTemp) + { + pairsWithGroupsFinal[pair] = new ReadOnlyCollection(groups); + } + + var groupsReadOnly = new ReadOnlyCollection(allGroupsList); + var pairsByUidReadOnly = new ReadOnlyDictionary(pairByUid); + var groupsByGidReadOnly = new ReadOnlyDictionary(allGroupsList.ToDictionary(g => g.Group.GID, g => g, StringComparer.Ordinal)); + + Pair? lastAddedPair = null; + var lastAdded = _pairManager.GetLastAddedUser(); + if (lastAdded is not null) + { + if (!pairByUid.TryGetValue(lastAdded.User.UID, out lastAddedPair)) + { + var groups = lastAdded.Groups.Keys + .Select(gid => + { + var result = _pairManager.GetGroup(gid); + return result.Success ? result.Value.GroupFullInfo : null; + }) + .Where(g => g is not null) + .Cast() + .ToList(); + + var entry = new PairDisplayEntry(new PairUniqueIdentifier(lastAdded.User.UID), lastAdded, groups, null); + lastAddedPair = _pairFactory.Create(entry); + } + } + + var snapshot = new PairUiSnapshot( + pairsByUidReadOnly, + directPairs, + new ReadOnlyDictionary>(groupPairsFinal), + new ReadOnlyDictionary>(pairsWithGroupsFinal), + groupsByGidReadOnly, + groupsReadOnly); + + return (snapshot, lastAddedPair); + } +} diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 7cdeb38..c30d5fa 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using LightlessSync.API.Data; using LightlessSync.API.Data.Comparer; +using LightlessSync.API.Data.Enum; using LightlessSync.API.Routes; using LightlessSync.FileCache; using LightlessSync.Interop.Ipc; @@ -16,8 +17,13 @@ using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; +using LightlessSync.Services.ActorTracking; +using LightlessSync.Services.Events; using LightlessSync.Services.Mediator; +using LightlessSync.Services.PairProcessing; using LightlessSync.Services.ServerConfiguration; +using LightlessSync.UI.Models; +using LightlessSync.UI.Services; using LightlessSync.UI.Style; using LightlessSync.Utils; using LightlessSync.UtilsEnum.Enum; @@ -27,11 +33,9 @@ using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.SignalR.Utils; using Microsoft.AspNetCore.Http.Connections; using Microsoft.Extensions.Logging; -using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; -using System.Linq; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Numerics; @@ -54,15 +58,16 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly FileUploadManager _fileTransferManager; private readonly FileTransferOrchestrator _fileTransferOrchestrator; private readonly IpcManager _ipcManager; - private readonly PairManager _pairManager; + private readonly ActorObjectService _actorObjectService; + private readonly PairUiService _pairUiService; private readonly PerformanceCollectorService _performanceCollector; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly PairProcessingLimiter _pairProcessingLimiter; + private readonly EventAggregator _eventAggregator; private readonly ServerConfigurationManager _serverConfigurationManager; private readonly UiSharedService _uiShared; private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress; private readonly NameplateService _nameplateService; - private readonly NameplateHandler _nameplateHandler; private (int, int, FileCacheEntity) _currentProgress; private bool _deleteAccountPopupModalShown = false; private bool _deleteFilesPopupModalShown = false; @@ -72,21 +77,75 @@ public class SettingsUi : WindowMediatorSubscriberBase private bool _readClearCache = false; private int _selectedEntry = -1; private string _uidToAddForIgnore = string.Empty; - private string _lightfinderIconInput = string.Empty; - private bool _lightfinderIconInputInitialized = false; - private int _lightfinderIconPresetIndex = -1; private bool _selectGeneralTabOnNextDraw = false; - private bool _openLightfinderSectionOnNextDraw = false; + private string _pairDebugFilter = string.Empty; + private bool _pairDebugVisibleOnly = true; + private bool _pairDiagnosticsEnabled; + private string? _selectedPairDebugUid = null; private static readonly LightlessConfig DefaultConfig = new(); - - private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[] + private static readonly JsonSerializerOptions DebugJsonOptions = new() { WriteIndented = true }; + private MainSettingsTab _selectedMainTab = MainSettingsTab.General; + private TransferSettingsTab _selectedTransferTab = TransferSettingsTab.Transfers; + private ServerSettingsTab _selectedServerTab = ServerSettingsTab.CharacterManagement; + private static readonly UiSharedService.TabOption[] MainTabOptions = new[] { - ("Link Marker", SeIconChar.LinkMarker), ("Hyadelyn", SeIconChar.Hyadelyn), ("Gil", SeIconChar.Gil), - ("Quest Sync", SeIconChar.QuestSync), ("Glamoured", SeIconChar.Glamoured), - ("Glamoured (Dyed)", SeIconChar.GlamouredDyed), ("Auto-Translate Open", SeIconChar.AutoTranslateOpen), - ("Auto-Translate Close", SeIconChar.AutoTranslateClose), ("Boxed Star", SeIconChar.BoxedStar), - ("Boxed Plus", SeIconChar.BoxedPlus) + new UiSharedService.TabOption("General", MainSettingsTab.General), + new UiSharedService.TabOption("Performance", MainSettingsTab.Performance), + new UiSharedService.TabOption("Storage", MainSettingsTab.Storage), + new UiSharedService.TabOption("Transfers", MainSettingsTab.Transfers), + new UiSharedService.TabOption("Service Settings", MainSettingsTab.ServiceSettings), + new UiSharedService.TabOption("Notifications", MainSettingsTab.Notifications), + new UiSharedService.TabOption("Debug", MainSettingsTab.Debug), }; + private readonly UiSharedService.TabOption[] _transferTabOptions = new UiSharedService.TabOption[2]; + private readonly List> _serverTabOptions = new(4); + private readonly string[] _generalTreeNavOrder = new[] + { + "Import & Export", + "Popup & Auto Fill", + "Behavior", + "Lightfinder", + "Pair List", + "Profiles", + "Colors", + "Server Info Bar", + "Nameplate", + }; + private static readonly HashSet _generalNavSeparatorAfter = new(StringComparer.Ordinal) + { + "Popup & Auto Fill", + "Profiles", + }; + private string? _generalScrollTarget = null; + private string? _generalOpenTreeTarget = null; + private readonly Dictionary _generalTreeHighlights = new(StringComparer.Ordinal); + private const float GeneralTreeHighlightDuration = 1.5f; + private readonly SeluneBrush _generalSeluneBrush = new(); + + private enum MainSettingsTab + { + General, + Performance, + Storage, + Transfers, + ServiceSettings, + Notifications, + Debug, + } + + private enum TransferSettingsTab + { + Transfers, + BlockedTransfers, + } + + private enum ServerSettingsTab + { + CharacterManagement, + SecretKeyManagement, + ServiceConfiguration, + PermissionSettings, + } private CancellationTokenSource? _validationCts; private Task>? _validationTask; @@ -94,10 +153,11 @@ public class SettingsUi : WindowMediatorSubscriberBase public SettingsUi(ILogger logger, UiSharedService uiShared, LightlessConfigService configService, UiThemeConfigService themeConfigService, - PairManager pairManager, + PairUiService pairUiService, ServerConfigurationManager serverConfigurationManager, PlayerPerformanceConfigService playerPerformanceConfigService, PairProcessingLimiter pairProcessingLimiter, + EventAggregator eventAggregator, LightlessMediator mediator, PerformanceCollectorService performanceCollector, FileUploadManager fileTransferManager, FileTransferOrchestrator fileTransferOrchestrator, @@ -106,15 +166,16 @@ public class SettingsUi : WindowMediatorSubscriberBase IpcManager ipcManager, CacheMonitor cacheMonitor, DalamudUtilService dalamudUtilService, HttpClient httpClient, NameplateService nameplateService, - NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings", + ActorObjectService actorObjectService) : base(logger, mediator, "Lightless Sync Settings", performanceCollector) { _configService = configService; _themeConfigService = themeConfigService; - _pairManager = pairManager; + _pairUiService = pairUiService; _serverConfigurationManager = serverConfigurationManager; _playerPerformanceConfigService = playerPerformanceConfigService; _pairProcessingLimiter = pairProcessingLimiter; + _eventAggregator = eventAggregator; _performanceCollector = performanceCollector; _fileTransferManager = fileTransferManager; _fileTransferOrchestrator = fileTransferOrchestrator; @@ -127,41 +188,22 @@ public class SettingsUi : WindowMediatorSubscriberBase _fileCompactor = fileCompactor; _uiShared = uiShared; _nameplateService = nameplateService; - _nameplateHandler = nameplateHandler; - AllowClickthrough = false; - AllowPinning = true; + _actorObjectService = actorObjectService; _validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v); - SizeConstraints = new WindowSizeConstraints() - { - MinimumSize = new Vector2(800, 400), MaximumSize = new Vector2(800, 2000), - }; - - TitleBarButtons = new() - { - new TitleBarButton() - { - Icon = FontAwesomeIcon.FileAlt, - Click = (msg) => - { - Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); - }, - IconOffset = new(2, 1), - ShowTooltip = () => - { - ImGui.BeginTooltip(); - ImGui.Text("View Update Notes"); - ImGui.EndTooltip(); - } - } - }; + WindowBuilder.For(this) + .AllowPinning(true) + .AllowClickthrough(false) + .SetSizeConstraints(new Vector2(900f, 400f), new Vector2(900f, 2000f)) + .AddTitleBarButton(FontAwesomeIcon.FileAlt, "View Update Notes", () => Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi)))) + .Apply(); Mediator.Subscribe(this, (_) => Toggle()); Mediator.Subscribe(this, (_) => { IsOpen = true; _selectGeneralTabOnNextDraw = true; - _openLightfinderSectionOnNextDraw = true; + FocusGeneralTree("Lightfinder"); }); Mediator.Subscribe(this, (_) => IsOpen = false); Mediator.Subscribe(this, (_) => UiSharedService_GposeStart()); @@ -449,6 +491,74 @@ public class SettingsUi : WindowMediatorSubscriberBase } } + private void DrawTextureDownscaleCounters() + { + HashSet trackedPairs = new(); + + var snapshot = _pairUiService.GetSnapshot(); + + foreach (var pair in snapshot.DirectPairs) + { + trackedPairs.Add(pair); + } + + foreach (var group in snapshot.GroupPairs.Values) + { + foreach (var pair in group) + { + trackedPairs.Add(pair); + } + } + + long totalOriginalBytes = 0; + long totalEffectiveBytes = 0; + var hasData = false; + + foreach (var pair in trackedPairs) + { + if (!pair.IsVisible) + continue; + + var original = pair.LastAppliedApproximateVRAMBytes; + var effective = pair.LastAppliedApproximateEffectiveVRAMBytes; + + if (original >= 0) + { + hasData = true; + totalOriginalBytes += original; + } + + if (effective >= 0) + { + hasData = true; + totalEffectiveBytes += effective; + } + } + + if (!hasData) + { + ImGui.TextDisabled("VRAM usage has not been calculated yet."); + return; + } + + var savedBytes = Math.Max(0L, totalOriginalBytes - totalEffectiveBytes); + var originalText = UiSharedService.ByteToString(totalOriginalBytes, addSuffix: true); + var effectiveText = UiSharedService.ByteToString(totalEffectiveBytes, addSuffix: true); + var savedText = UiSharedService.ByteToString(savedBytes, addSuffix: true); + + ImGui.TextUnformatted($"Total VRAM usage (original): {originalText}"); + ImGui.TextUnformatted($"Total VRAM usage (effective): {effectiveText}"); + + if (savedBytes > 0) + { + UiSharedService.ColorText($"VRAM saved by downscaling: {savedText}", UIColors.Get("LightlessGreen")); + } + else + { + ImGui.TextUnformatted($"VRAM saved by downscaling: {savedText}"); + } + } + private void DrawThemeVectorRow(MainStyle.StyleVector2Option option) { ImGui.TableNextRow(); @@ -539,7 +649,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } } - private bool DrawStyleResetButton(string key, bool hasOverride, string? tooltipOverride = null) + private static bool DrawStyleResetButton(string key, bool hasOverride, string? tooltipOverride = null) { using var id = ImRaii.PushId($"reset-{key}"); using var disabled = ImRaii.Disabled(!hasOverride); @@ -663,7 +773,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText("Controls how many uploads can run at once."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); if (ImGui.Checkbox("Enable Pair Download Limiter", ref limitPairApplications)) { @@ -710,7 +820,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.TextColored(ImGuiColors.DalamudGrey, "Pair apply limiter is disabled."); } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f); if (ImGui.Checkbox("Use Alternative Upload Method", ref useAlternativeUpload)) { @@ -744,11 +854,37 @@ public class SettingsUi : WindowMediatorSubscriberBase $"D = Decompressing download"); if (!_configService.Current.ShowTransferWindow) ImGui.BeginDisabled(); ImGui.Indent(); + bool editTransferWindowPosition = _uiShared.EditTrackerPosition; if (ImGui.Checkbox("Edit Transfer Window position", ref editTransferWindowPosition)) { _uiShared.EditTrackerPosition = editTransferWindowPosition; } + bool showPlayerLinesTransferWindow = _configService.Current.ShowPlayerLinesTransferWindow; + + if (ImGui.Checkbox("Toggle the player lines in the Transfer Window", ref showPlayerLinesTransferWindow)) + { + _configService.Current.ShowPlayerLinesTransferWindow = showPlayerLinesTransferWindow; + _configService.Save(); + } + + if (!showPlayerLinesTransferWindow) + { + _configService.Current.ShowPlayerSpeedBarsTransferWindow = false; + ImGui.BeginDisabled(); + } + + bool showPlayerSpeedBarsTransferWindow = _configService.Current.ShowPlayerSpeedBarsTransferWindow; + if (ImGui.Checkbox("Toggle the download bars in player lines in the Transfer Window", ref showPlayerSpeedBarsTransferWindow)) + { + _configService.Current.ShowPlayerSpeedBarsTransferWindow = showPlayerSpeedBarsTransferWindow; + _configService.Save(); + } + + if (!showPlayerLinesTransferWindow) + { + ImGui.EndDisabled(); + } ImGui.Unindent(); if (!_configService.Current.ShowTransferWindow) ImGui.EndDisabled(); @@ -826,13 +962,10 @@ public class SettingsUi : WindowMediatorSubscriberBase using var tree = ImRaii.TreeNode("Speed Test to Servers"); if (tree) { - if (_downloadServersTask == null || ((_downloadServersTask?.IsCompleted ?? false) && - (!_downloadServersTask?.IsCompletedSuccessfully ?? false))) + if ((_downloadServersTask == null || ((_downloadServersTask?.IsCompleted ?? false) && + (!_downloadServersTask?.IsCompletedSuccessfully ?? false))) && _uiShared.IconTextButton(FontAwesomeIcon.GroupArrowsRotate, "Update Download Server List")) { - if (_uiShared.IconTextButton(FontAwesomeIcon.GroupArrowsRotate, "Update Download Server List")) - { - _downloadServersTask = GetDownloadServerList(); - } + _downloadServersTask = GetDownloadServerList(); } if (_downloadServersTask != null && _downloadServersTask.IsCompleted && @@ -894,89 +1027,96 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.UnderlinedBigText("Current Transfers", UIColors.Get("LightlessBlue")); ImGuiHelpers.ScaledDummy(5); - if (ImGui.BeginTabBar("TransfersTabBar")) + _transferTabOptions[0] = new UiSharedService.TabOption( + "Transfers", + TransferSettingsTab.Transfers, + _apiController.ServerState is ServerState.Connected); + _transferTabOptions[1] = new UiSharedService.TabOption( + "Blocked Transfers", + TransferSettingsTab.BlockedTransfers); + + UiSharedService.Tab("TransferSettingsTabs", _transferTabOptions, ref _selectedTransferTab); + ImGuiHelpers.ScaledDummy(5); + + switch (_selectedTransferTab) { - if (ApiController.ServerState is ServerState.Connected && ImGui.BeginTabItem("Transfers")) - { - var uploadsSnapshot = _fileTransferManager.GetCurrentUploadsSnapshot(); - var activeUploads = uploadsSnapshot.Count(c => !c.IsTransferred); - var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8); - ImGui.TextUnformatted($"Uploads (slots {activeUploads}/{uploadSlotLimit})"); - if (ImGui.BeginTable("UploadsTable", 3)) + case TransferSettingsTab.Transfers when _apiController.ServerState is ServerState.Connected: { - ImGui.TableSetupColumn("File"); - ImGui.TableSetupColumn("Uploaded"); - ImGui.TableSetupColumn("Size"); - ImGui.TableHeadersRow(); - foreach (var transfer in uploadsSnapshot) + var uploadsSnapshot = _fileTransferManager.GetCurrentUploadsSnapshot(); + var activeUploads = uploadsSnapshot.Count(c => !c.IsTransferred); + var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8); + ImGui.TextUnformatted($"Uploads (slots {activeUploads}/{uploadSlotLimit})"); + if (ImGui.BeginTable("UploadsTable", 3)) { - var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total)); - using var col = ImRaii.PushColor(ImGuiCol.Text, color); - ImGui.TableNextColumn(); - if (transfer is UploadFileTransfer uploadTransfer) + ImGui.TableSetupColumn("File"); + ImGui.TableSetupColumn("Uploaded"); + ImGui.TableSetupColumn("Size"); + ImGui.TableHeadersRow(); + foreach (var transfer in uploadsSnapshot) { - ImGui.TextUnformatted(uploadTransfer.LocalFile); - } - else - { - ImGui.TextUnformatted(transfer.Hash); + var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total)); + using var col = ImRaii.PushColor(ImGuiCol.Text, color); + ImGui.TableNextColumn(); + if (transfer is UploadFileTransfer uploadTransfer) + { + ImGui.TextUnformatted(uploadTransfer.LocalFile); + } + else + { + ImGui.TextUnformatted(transfer.Hash); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred)); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Total)); } - ImGui.TableNextColumn(); - ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred)); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Total)); + ImGui.EndTable(); } - ImGui.EndTable(); - } - - ImGui.Separator(); - ImGui.TextUnformatted("Downloads"); - if (ImGui.BeginTable("DownloadsTable", 4)) - { - ImGui.TableSetupColumn("User"); - ImGui.TableSetupColumn("Server"); - ImGui.TableSetupColumn("Files"); - ImGui.TableSetupColumn("Download"); - ImGui.TableHeadersRow(); - - foreach (var transfer in _currentDownloads.ToArray()) + ImGui.Separator(); + ImGui.TextUnformatted("Downloads"); + if (ImGui.BeginTable("DownloadsTable", 4)) { - var userName = transfer.Key.Name; - foreach (var entry in transfer.Value) + ImGui.TableSetupColumn("User"); + ImGui.TableSetupColumn("Server"); + ImGui.TableSetupColumn("Files"); + ImGui.TableSetupColumn("Download"); + ImGui.TableHeadersRow(); + + foreach (var transfer in _currentDownloads.ToArray()) { - var color = UiSharedService.UploadColor((entry.Value.TransferredBytes, - entry.Value.TotalBytes)); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(userName); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(entry.Key); - var col = ImRaii.PushColor(ImGuiCol.Text, color); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(entry.Value.TransferredFiles + "/" + entry.Value.TotalFiles); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(UiSharedService.ByteToString(entry.Value.TransferredBytes) + "/" + - UiSharedService.ByteToString(entry.Value.TotalBytes)); - ImGui.TableNextColumn(); - col.Dispose(); - ImGui.TableNextRow(); + var userName = transfer.Key.Name; + foreach (var entry in transfer.Value) + { + var color = UiSharedService.UploadColor((entry.Value.TransferredBytes, + entry.Value.TotalBytes)); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(userName); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.Key); + var col = ImRaii.PushColor(ImGuiCol.Text, color); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.Value.TransferredFiles + "/" + entry.Value.TotalFiles); + ImGui.TableNextColumn(); + ImGui.TextUnformatted( + UiSharedService.ByteToString(entry.Value.TransferredBytes) + "/" + + UiSharedService.ByteToString(entry.Value.TotalBytes)); + ImGui.TableNextColumn(); + col.Dispose(); + ImGui.TableNextRow(); + } } + + ImGui.EndTable(); } - ImGui.EndTable(); + break; } - - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Blocked Transfers")) - { + case TransferSettingsTab.BlockedTransfers: DrawBlockedTransfers(); - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); + break; } } @@ -1063,9 +1203,9 @@ public class SettingsUi : WindowMediatorSubscriberBase .DeserializeAsync>(await result.Content.ReadAsStreamAsync().ConfigureAwait(false)) .ConfigureAwait(false); } - catch (Exception ex) + catch (Exception) { - _logger.LogWarning(ex, "Failed to get download server list"); + _logger.LogWarning("Failed to get download server list"); throw; } } @@ -1146,9 +1286,441 @@ public class SettingsUi : WindowMediatorSubscriberBase UiSharedService.TooltipSeparator + "Keeping LOD enabled can lead to more crashes. Use at your own risk."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 2f); + ImGuiHelpers.ScaledDummy(10f); + DrawPairDebugPanel(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 2f); } + private void DrawPairDebugPanel() + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow")); + ImGui.TextUnformatted("Pair Diagnostics"); + ImGui.PopStyleColor(); + ImGuiHelpers.ScaledDummy(3f); + + ImGui.Checkbox("Enable Pair Diagnostics", ref _pairDiagnosticsEnabled); + UiSharedService.AttachToolTip("When disabled the UI stops querying pair handlers and no diagnostics are processed."); + + if (!_pairDiagnosticsEnabled) + { + UiSharedService.ColorTextWrapped("Diagnostics are disabled. Enable the toggle above to inspect active pairs.", UIColors.Get("LightlessYellow")); + return; + } + + var snapshot = _pairUiService.GetSnapshot(); + if (snapshot.PairsByUid.Count == 0) + { + UiSharedService.ColorTextWrapped("No pairs are currently tracked. Connect to the service and re-open this panel.", UIColors.Get("LightlessYellow")); + return; + } + + ImGui.SetNextItemWidth(280f * ImGuiHelpers.GlobalScale); + ImGui.InputTextWithHint("##pairDebugFilter", "Search by UID, alias, or player name...", ref _pairDebugFilter, 96); + UiSharedService.AttachToolTip("Filters the list by UID, aliases, or currently cached player name."); + ImGui.SameLine(); + ImGui.Checkbox("Visible pairs only", ref _pairDebugVisibleOnly); + UiSharedService.AttachToolTip("When enabled only currently visible pairs remain in the list."); + + var pairs = snapshot.PairsByUid.Values; + var filteredPairs = pairs + .Where(p => !_pairDebugVisibleOnly || p.IsVisible) + .Where(p => PairMatchesFilter(p, _pairDebugFilter)) + .OrderByDescending(p => p.IsVisible) + .ThenByDescending(p => p.IsOnline) + .ThenBy(p => p.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (filteredPairs.Count == 0) + { + UiSharedService.ColorTextWrapped("No pairs match the current filters.", UIColors.Get("LightlessYellow")); + _selectedPairDebugUid = null; + return; + } + + if (_selectedPairDebugUid is null || !filteredPairs.Any(p => string.Equals(p.UserData.UID, _selectedPairDebugUid, StringComparison.Ordinal))) + { + _selectedPairDebugUid = filteredPairs[0].UserData.UID; + } + + if (_selectedPairDebugUid is null || !snapshot.PairsByUid.TryGetValue(_selectedPairDebugUid, out var selectedPair)) + { + selectedPair = filteredPairs[0]; + } + + var visibleCount = pairs.Count(p => p.IsVisible); + var onlineCount = pairs.Count(p => p.IsOnline); + var totalPairs = snapshot.PairsByUid.Count; + ImGui.TextUnformatted($"Visible: {visibleCount} / {totalPairs}; Online: {onlineCount}"); + + var mainChildHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 12f, ImGui.GetContentRegionAvail().Y * 0.95f); + if (ImGui.BeginChild("##pairDebugPanel", new Vector2(-1, mainChildHeight), true, ImGuiWindowFlags.HorizontalScrollbar)) + { + var childAvail = ImGui.GetContentRegionAvail(); + var leftWidth = MathF.Max(220f * ImGuiHelpers.GlobalScale, childAvail.X * 0.35f); + leftWidth = MathF.Min(leftWidth, childAvail.X * 0.6f); + if (ImGui.BeginChild("##pairDebugList", new Vector2(leftWidth, 0), true, ImGuiWindowFlags.HorizontalScrollbar)) + { + if (ImGui.BeginTable("##pairDebugTable", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollY | ImGuiTableFlags.ScrollX)) + { + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 20f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Pair"); + ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 90f * ImGuiHelpers.GlobalScale); + ImGui.TableHeadersRow(); + + foreach (var entry in filteredPairs) + { + var isSelected = string.Equals(entry.UserData.UID, _selectedPairDebugUid, StringComparison.Ordinal); + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + DrawPairStateIndicator(entry); + + ImGui.TableNextColumn(); + if (ImGui.Selectable($"{entry.UserData.AliasOrUID}##pairDebugSelect_{entry.UserData.UID}", isSelected, ImGuiSelectableFlags.SpanAllColumns)) + { + _selectedPairDebugUid = entry.UserData.UID; + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip($"UID: {entry.UserData.UID}\nVisible: {entry.IsVisible}\nOnline: {entry.IsOnline}\nDirect pair: {entry.IsDirectlyPaired}"); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.IsVisible ? "Visible" : entry.IsOnline ? "Online" : "Offline"); + } + + ImGui.EndTable(); + } + } + ImGui.EndChild(); + + ImGui.SameLine(); + + if (ImGui.BeginChild("##pairDebugDetails", new Vector2(0, 0), true, ImGuiWindowFlags.HorizontalScrollbar)) + { + DrawPairDebugDetails(selectedPair, snapshot); + } + ImGui.EndChild(); + } + ImGui.EndChild(); + } + + private static bool PairMatchesFilter(Pair pair, string filter) + { + if (string.IsNullOrWhiteSpace(filter)) + { + return true; + } + + return pair.UserData.UID.Contains(filter, StringComparison.OrdinalIgnoreCase) + || pair.UserData.AliasOrUID.Contains(filter, StringComparison.OrdinalIgnoreCase) + || (!string.IsNullOrEmpty(pair.PlayerName) && pair.PlayerName.Contains(filter, StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrEmpty(pair.Ident) && pair.Ident.Contains(filter, StringComparison.OrdinalIgnoreCase)); + } + + private static void DrawPairStateIndicator(Pair pair) + { + var color = pair.IsVisible + ? UIColors.Get("LightlessGreen") + : pair.IsOnline ? UIColors.Get("LightlessYellow") + : UIColors.Get("DimRed"); + + var drawList = ImGui.GetWindowDrawList(); + var cursor = ImGui.GetCursorScreenPos(); + var radius = ImGui.GetTextLineHeight() * 0.35f; + var center = cursor + new Vector2(radius, radius); + drawList.AddCircleFilled(center, radius, ImGui.ColorConvertFloat4ToU32(color)); + ImGui.Dummy(new Vector2(radius * 2f, radius * 2f)); + } + + private void DrawPairDebugDetails(Pair pair, PairUiSnapshot snapshot) + { + var debugInfo = pair.GetDebugInfo(); + var statusColor = pair.IsVisible + ? UIColors.Get("LightlessGreen") + : pair.IsOnline ? UIColors.Get("LightlessYellow") + : UIColors.Get("DimRed"); + + ImGui.TextColored(statusColor, pair.UserData.AliasOrUID); + ImGui.SameLine(); + ImGui.TextColored(statusColor, $"[{(pair.IsVisible ? "Visible" : pair.IsOnline ? "Online" : "Offline")}]"); + + if (ImGui.BeginTable("##pairDebugProperties", 2, ImGuiTableFlags.SizingStretchProp)) + { + DrawPairPropertyRow("UID", pair.UserData.UID); + DrawPairPropertyRow("Alias", string.IsNullOrEmpty(pair.UserData.Alias) ? "(none)" : pair.UserData.Alias!); + DrawPairPropertyRow("Player Name", pair.PlayerName ?? "(not cached)"); + DrawPairPropertyRow("Handler Ident", string.IsNullOrEmpty(pair.Ident) ? "(not bound)" : pair.Ident); + DrawPairPropertyRow("Character Id", FormatCharacterId(pair.PlayerCharacterId)); + DrawPairPropertyRow("Direct Pair", FormatBool(pair.IsDirectlyPaired)); + DrawPairPropertyRow("Individual Status", pair.IndividualPairStatus.ToString()); + DrawPairPropertyRow("Any Connection", FormatBool(pair.HasAnyConnection())); + DrawPairPropertyRow("Paused", FormatBool(pair.IsPaused)); + DrawPairPropertyRow("Visible", FormatBool(pair.IsVisible), statusColor); + DrawPairPropertyRow("Online", FormatBool(pair.IsOnline)); + DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler)); + DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized)); + DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible)); + DrawPairPropertyRow("Last Time person rendered in", FormatTimestamp(debugInfo.InvisibleSinceUtc)); + DrawPairPropertyRow("Handler Timer Temp Collection removal", FormatCountdown(debugInfo.VisibilityEvictionRemainingSeconds)); + DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion)); + + DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)"); + ImGui.EndTable(); + } + + ImGui.Separator(); + ImGui.TextUnformatted("Applied Data"); + if (ImGui.BeginTable("##pairDebugDataStats", 2, ImGuiTableFlags.SizingStretchProp)) + { + DrawPairPropertyRow("Last Data Size", FormatBytes(pair.LastAppliedDataBytes)); + DrawPairPropertyRow("Approx. VRAM", FormatBytes(pair.LastAppliedApproximateVRAMBytes)); + DrawPairPropertyRow("Effective VRAM", FormatBytes(pair.LastAppliedApproximateEffectiveVRAMBytes)); + DrawPairPropertyRow("Last Triangles", pair.LastAppliedDataTris < 0 ? "n/a" : pair.LastAppliedDataTris.ToString(CultureInfo.InvariantCulture)); + ImGui.EndTable(); + } + + var lastData = pair.LastReceivedCharacterData; + if (lastData is null) + { + ImGui.TextDisabled("No character data has been received for this pair."); + } + else + { + var fileReplacementCount = lastData.FileReplacements.Values.Sum(list => list?.Count ?? 0); + var totalGamePaths = lastData.FileReplacements.Values.Sum(list => list?.Sum(replacement => replacement.GamePaths.Length) ?? 0); + ImGui.BulletText($"File replacements: {fileReplacementCount} entries across {totalGamePaths} game paths."); + ImGui.BulletText($"Customize+: {lastData.CustomizePlusData.Count}, Glamourer entries: {lastData.GlamourerData.Count}"); + ImGui.BulletText($"Manipulation length: {lastData.ManipulationData.Length}, Heels set: {FormatBool(!string.IsNullOrEmpty(lastData.HeelsData))}"); + + if (ImGui.TreeNode("Last Received Character Data (JSON)")) + { + DrawJsonBlob(lastData); + ImGui.TreePop(); + } + } + + ImGui.Separator(); + ImGui.TextUnformatted("Application Timeline"); + if (ImGui.BeginTable("##pairDebugTimeline", 2, ImGuiTableFlags.SizingStretchProp)) + { + DrawPairPropertyRow("Last Data Received", FormatTimestamp(debugInfo.LastDataReceivedAt)); + DrawPairPropertyRow("Last Apply Attempt", FormatTimestamp(debugInfo.LastApplyAttemptAt)); + DrawPairPropertyRow("Last Successful Apply", FormatTimestamp(debugInfo.LastSuccessfulApplyAt)); + ImGui.EndTable(); + } + + if (!string.IsNullOrEmpty(debugInfo.LastFailureReason)) + { + UiSharedService.ColorTextWrapped($"Last failure: {debugInfo.LastFailureReason}", UIColors.Get("DimRed")); + if (debugInfo.BlockingConditions.Count > 0) + { + ImGui.TextUnformatted("Blocking conditions:"); + foreach (var condition in debugInfo.BlockingConditions) + { + ImGui.BulletText(condition); + } + } + } + + ImGui.Separator(); + ImGui.TextUnformatted("Application & Download State"); + if (ImGui.BeginTable("##pairDebugProcessing", 2, ImGuiTableFlags.SizingStretchProp)) + { + DrawPairPropertyRow("Applying Data", FormatBool(debugInfo.IsApplying)); + DrawPairPropertyRow("Downloading", FormatBool(debugInfo.IsDownloading)); + DrawPairPropertyRow("Pending Downloads", debugInfo.PendingDownloadCount.ToString(CultureInfo.InvariantCulture)); + DrawPairPropertyRow("Forbidden Downloads", debugInfo.ForbiddenDownloadCount.ToString(CultureInfo.InvariantCulture)); + ImGui.EndTable(); + } + + ImGui.Separator(); + ImGui.TextUnformatted("Syncshell Memberships"); + if (snapshot.PairsWithGroups.TryGetValue(pair, out var groups) && groups.Count > 0) + { + foreach (var group in groups.OrderBy(g => g.Group.AliasOrGID, StringComparer.OrdinalIgnoreCase)) + { + var flags = group.GroupPairUserInfos.TryGetValue(pair.UserData.UID, out var info) ? info : GroupPairUserInfo.None; + var flagLabel = flags switch + { + GroupPairUserInfo.None => string.Empty, + _ => $" ({string.Join(", ", GetGroupInfoFlags(flags))})" + }; + ImGui.BulletText($"{group.Group.AliasOrGID} [{group.Group.GID}]{flagLabel}"); + } + } + else + { + ImGui.TextDisabled("Not a member of any syncshells."); + } + + if (pair.UserPair is null) + { + ImGui.TextDisabled("Pair DTO snapshot unavailable."); + } + else if (ImGui.TreeNode("Pair DTO Snapshot")) + { + DrawJsonBlob(pair.UserPair); + ImGui.TreePop(); + } + + ImGui.Separator(); + DrawPairEventLog(pair); + } + + private static IEnumerable GetGroupInfoFlags(GroupPairUserInfo info) + { + if (info.HasFlag(GroupPairUserInfo.IsModerator)) + { + yield return "Moderator"; + } + + if (info.HasFlag(GroupPairUserInfo.IsPinned)) + { + yield return "Pinned"; + } + } + + private void DrawPairEventLog(Pair pair) + { + ImGui.TextUnformatted("Recent Events"); + var events = _eventAggregator.EventList.Value; + var alias = pair.UserData.Alias; + var aliasOrUid = pair.UserData.AliasOrUID; + var rawUid = pair.UserData.UID; + var playerName = pair.PlayerName; + + var relevantEvents = events.Where(e => + EventMatchesIdentifier(e, rawUid) + || EventMatchesIdentifier(e, aliasOrUid) + || EventMatchesIdentifier(e, alias) + || (!string.IsNullOrEmpty(playerName) && string.Equals(e.Character, playerName, StringComparison.OrdinalIgnoreCase))) + .OrderByDescending(e => e.EventTime) + .Take(40) + .ToList(); + + if (relevantEvents.Count == 0) + { + ImGui.TextDisabled("No recent events were logged for this pair."); + return; + } + + var baseTableHeight = 300f * ImGuiHelpers.GlobalScale; + var tableHeight = MathF.Max(baseTableHeight, ImGui.GetContentRegionAvail().Y); + if (ImGui.BeginTable("##pairDebugEvents", 3, + ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY, + new Vector2(0f, tableHeight))) + { + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Time", ImGuiTableColumnFlags.WidthFixed, 110f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 60f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Details"); + ImGui.TableHeadersRow(); + + foreach (var ev in relevantEvents) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(ev.EventTime.ToString("T", CultureInfo.CurrentCulture)); + + ImGui.TableNextColumn(); + var (icon, color) = ev.EventSeverity switch + { + EventSeverity.Informational => (FontAwesomeIcon.InfoCircle, UIColors.Get("LightlessGreen")), + EventSeverity.Warning => (FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow")), + EventSeverity.Error => (FontAwesomeIcon.ExclamationCircle, UIColors.Get("DimRed")), + _ => (FontAwesomeIcon.QuestionCircle, UIColors.Get("LightlessGrey")) + }; + _uiShared.IconText(icon, color); + UiSharedService.AttachToolTip(ev.EventSeverity.ToString()); + + ImGui.TableNextColumn(); + ImGui.TextWrapped($"[{ev.EventSource}] {ev.Message}"); + } + + ImGui.EndTable(); + } + } + + private static bool EventMatchesIdentifier(Event evt, string? identifier) + { + if (string.IsNullOrWhiteSpace(identifier)) + { + return false; + } + + return (!string.IsNullOrEmpty(evt.UserId) && string.Equals(evt.UserId, identifier, StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrEmpty(evt.AliasOrUid) && string.Equals(evt.AliasOrUid, identifier, StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrEmpty(evt.UID) && string.Equals(evt.UID, identifier, StringComparison.OrdinalIgnoreCase)); + } + + private static void DrawJsonBlob(object? value) + { + if (value is null) + { + ImGui.TextDisabled("(null)"); + return; + } + + try + { + var json = JsonSerializer.Serialize(value, DebugJsonOptions); + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGrey")); + foreach (var line in json.Split('\n')) + { + ImGui.TextUnformatted(line); + } + ImGui.PopStyleColor(); + } + catch (Exception ex) + { + UiSharedService.ColorTextWrapped($"Failed to serialize data: {ex.Message}", UIColors.Get("DimRed")); + } + } + + private static void DrawPairPropertyRow(string label, string value, Vector4? colorOverride = null) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(label); + ImGui.TableNextColumn(); + if (colorOverride is { } color) + { + ImGui.TextColored(color, value); + } + else + { + ImGui.TextUnformatted(value); + } + } + + private static string FormatTimestamp(DateTime? value) + { + return value is null ? "n/a" : value.Value.ToLocalTime().ToString("G", CultureInfo.CurrentCulture); + } + + private static string? FormatCountdown(double? remainingSeconds) + { + if (!remainingSeconds.HasValue) + return "No"; + + var secs = Math.Max(0, remainingSeconds.Value); + var t = TimeSpan.FromSeconds(secs); + + return t.TotalHours >= 1 + ? $"{(int)t.TotalHours:00}:{t.Minutes:00}:{t.Seconds:00}" + : $"{(int)t.TotalMinutes:00}:{t.Seconds:00}"; + } + + private static string FormatBytes(long value) => value < 0 ? "n/a" : UiSharedService.ByteToString(value); + + private static string FormatCharacterId(uint id) => id == uint.MaxValue ? "n/a" : $"{id} (0x{id:X8})"; + + private static string FormatBool(bool value) => value ? "Yes" : "No"; + + private void DrawFileStorageSettings() { _lastTab = "FileCache"; @@ -1348,7 +1920,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } } - _uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); ImGui.TreePop(); } @@ -1380,7 +1952,23 @@ public class SettingsUi : WindowMediatorSubscriberBase } catch (IOException ex) { - _logger.LogWarning(ex, $"Could not delete file {file} because it is in use."); + _logger.LogWarning(ex, "Could not delete file {file} because it is in use.", file); + } + } + + foreach (var directory in Directory.GetDirectories(_configService.Current.CacheFolder)) + { + try + { + Directory.Delete(directory, recursive: true); + } + catch (IOException ex) + { + _logger.LogWarning(ex, "Could not delete directory {Directory} because it is in use.", directory); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogWarning(ex, "Could not delete directory {Directory} due to access restrictions.", directory); } } }); @@ -1398,7 +1986,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndDisabled(); ImGui.Unindent(); - _uiShared.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); ImGui.TreePop(); } } @@ -1411,74 +1999,101 @@ public class SettingsUi : WindowMediatorSubscriberBase } _lastTab = "General"; - //UiSharedService.FontText("Experimental", _uiShared.UidFont); - //ImGui.Separator(); + + using var generalSelune = Selune.Begin(_generalSeluneBrush, ImGui.GetWindowDrawList(), ImGui.GetWindowPos(), ImGui.GetWindowSize()); + + var navAvailableWidth = ImGui.GetContentRegionAvail().X; + var minNavWidth = 80f * ImGuiHelpers.GlobalScale; + var maxNavWidth = 150f * ImGuiHelpers.GlobalScale; + var navWidth = Math.Max(minNavWidth, Math.Min(maxNavWidth, navAvailableWidth * 0.24f)); + var navHeight = MathF.Max(ImGui.GetContentRegionAvail().Y, 400f * ImGuiHelpers.GlobalScale); + var style = ImGui.GetStyle(); + + ImGui.BeginGroup(); + ImGui.BeginChild("GeneralNavigation", new Vector2(navWidth, navHeight), true); + DrawGeneralNavigation(); + ImGui.EndChild(); + ImGui.EndGroup(); + + ImGui.SameLine(0, style.ItemSpacing.X * 1.75f); + + ImGui.BeginGroup(); + ImGui.BeginChild("GeneralSettingsContent", new Vector2(0, navHeight), false); _uiShared.UnderlinedBigText("General Settings", UIColors.Get("LightlessBlue")); ImGui.Dummy(new Vector2(10)); _uiShared.BigText("Notes"); - if (_uiShared.MediumTreeNode("Import & Export", UIColors.Get("LightlessPurple"))) + using (var importExportTree = BeginGeneralTree("Import & Export", UIColors.Get("LightlessPurple"))) { - if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard")) + if (importExportTree.Visible) { - ImGui.SetClipboardText(UiSharedService.GetNotes(_pairManager.DirectPairs - .UnionBy(_pairManager.GroupPairs.SelectMany(p => p.Value), p => p.UserData, - UserDataComparer.Instance).ToList())); - } + if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard")) + { + var snapshot = _pairUiService.GetSnapshot(); + ImGui.SetClipboardText(UiSharedService.GetNotes(snapshot.DirectPairs + .UnionBy(snapshot.GroupPairs.SelectMany(p => p.Value), p => p.UserData, + UserDataComparer.Instance).ToList())); + } - if (_uiShared.IconTextButton(FontAwesomeIcon.FileImport, "Import notes from clipboard")) - { - _notesSuccessfullyApplied = null; - var notes = ImGui.GetClipboardText(); - _notesSuccessfullyApplied = _uiShared.ApplyNotesFromClipboard(notes, _overwriteExistingLabels); - } + if (_uiShared.IconTextButton(FontAwesomeIcon.FileImport, "Import notes from clipboard")) + { + _notesSuccessfullyApplied = null; + var notes = ImGui.GetClipboardText(); + _notesSuccessfullyApplied = _uiShared.ApplyNotesFromClipboard(notes, _overwriteExistingLabels); + } - ImGui.SameLine(); - ImGui.Checkbox("Overwrite existing notes", ref _overwriteExistingLabels); - _uiShared.DrawHelpText( - "If this option is selected all already existing notes for UIDs will be overwritten by the imported notes."); - if (_notesSuccessfullyApplied.HasValue && _notesSuccessfullyApplied.Value) - { - UiSharedService.ColorTextWrapped("User Notes successfully imported", UIColors.Get("LightlessBlue")); - } - else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value) - { - UiSharedService.ColorTextWrapped( - "Attempt to import notes from clipboard failed. Check formatting and try again", - ImGuiColors.DalamudRed); - } + ImGui.SameLine(); + ImGui.Checkbox("Overwrite existing notes", ref _overwriteExistingLabels); + _uiShared.DrawHelpText( + "If this option is selected all already existing notes for UIDs will be overwritten by the imported notes."); + if (_notesSuccessfullyApplied.HasValue && _notesSuccessfullyApplied.Value) + { + UiSharedService.ColorTextWrapped("User Notes successfully imported", UIColors.Get("LightlessBlue")); + } + else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value) + { + UiSharedService.ColorTextWrapped( + "Attempt to import notes from clipboard failed. Check formatting and try again", + ImGuiColors.DalamudRed); + } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + importExportTree.MarkContentEnd(); + } } ImGui.Separator(); var openPopupOnAddition = _configService.Current.OpenPopupOnAdd; - if (_uiShared.MediumTreeNode("Popup & Auto Fill", UIColors.Get("LightlessPurple"))) + using (var popupTree = BeginGeneralTree("Popup & Auto Fill", UIColors.Get("LightlessPurple"))) { - if (ImGui.Checkbox("Open Notes Popup on user addition", ref openPopupOnAddition)) + if (popupTree.Visible) { - _configService.Current.OpenPopupOnAdd = openPopupOnAddition; - _configService.Save(); + if (ImGui.Checkbox("Open Notes Popup on user addition", ref openPopupOnAddition)) + { + _configService.Current.OpenPopupOnAdd = openPopupOnAddition; + _configService.Save(); + } + + _uiShared.DrawHelpText( + "This will open a popup that allows you to set the notes for a user after successfully adding them to your individual pairs."); + + var autoPopulateNotes = _configService.Current.AutoPopulateEmptyNotesFromCharaName; + if (ImGui.Checkbox("Automatically populate notes using player names", ref autoPopulateNotes)) + { + _configService.Current.AutoPopulateEmptyNotesFromCharaName = autoPopulateNotes; + _configService.Save(); + } + + _uiShared.DrawHelpText( + "This will automatically populate user notes using the first encountered player name if the note was not set prior"); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + popupTree.MarkContentEnd(); } - - _uiShared.DrawHelpText( - "This will open a popup that allows you to set the notes for a user after successfully adding them to your individual pairs."); - - var autoPopulateNotes = _configService.Current.AutoPopulateEmptyNotesFromCharaName; - if (ImGui.Checkbox("Automatically populate notes using player names", ref autoPopulateNotes)) - { - _configService.Current.AutoPopulateEmptyNotesFromCharaName = autoPopulateNotes; - _configService.Save(); - } - - _uiShared.DrawHelpText( - "This will automatically populate user notes using the first encountered player name if the note was not set prior"); - - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); } ImGui.Separator(); @@ -1507,68 +2122,61 @@ public class SettingsUi : WindowMediatorSubscriberBase var groupedSyncshells = _configService.Current.ShowGroupedSyncshellsInAll; var groupInVisible = _configService.Current.ShowSyncshellUsersInVisible; var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately; + var greenVisiblePair = _configService.Current.ShowVisiblePairsGreenEye; - - if (_uiShared.MediumTreeNode("Behavior", UIColors.Get("LightlessPurple"))) + using (var behaviorTree = BeginGeneralTree("Behavior", UIColors.Get("LightlessPurple"))) { - if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu)) + if (behaviorTree.Visible) { - _configService.Current.EnableRightClickMenus = enableRightClickMenu; - _configService.Save(); - } - - _uiShared.DrawHelpText("This will add all Lightless related right click menu entries in the game UI."); - - if (ImGui.Checkbox("Display status and visible pair count in Server Info Bar", ref enableDtrEntry)) - { - _configService.Current.EnableDtrEntry = enableDtrEntry; - _configService.Save(); - } - - _uiShared.DrawHelpText( - "This will add Lightless connection status and visible pair count in the Server Info Bar.\nYou can further configure this through your Dalamud Settings."); - - using (ImRaii.Disabled(!enableDtrEntry)) - { - using var indent = ImRaii.PushIndent(); - if (ImGui.Checkbox("Show visible character's UID in tooltip", ref showUidInDtrTooltip)) + if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu)) { - _configService.Current.ShowUidInDtrTooltip = showUidInDtrTooltip; + _configService.Current.EnableRightClickMenus = enableRightClickMenu; _configService.Save(); } - if (ImGui.Checkbox("Prefer notes over player names in tooltip", ref preferNoteInDtrTooltip)) + _uiShared.DrawHelpText("This will add all Lightless related right click menu entries in the game UI."); + + if (ImGui.Checkbox("Display status and visible pair count in Server Info Bar", ref enableDtrEntry)) { - _configService.Current.PreferNoteInDtrTooltip = preferNoteInDtrTooltip; + _configService.Current.EnableDtrEntry = enableDtrEntry; _configService.Save(); } - } + _uiShared.DrawHelpText( + "This will add Lightless connection status and visible pair count in the Server Info Bar.\nYou can further configure this through your Dalamud Settings."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); + using (ImRaii.Disabled(!enableDtrEntry)) + { + using var indent = ImRaii.PushIndent(); + if (ImGui.Checkbox("Show visible character's UID in tooltip", ref showUidInDtrTooltip)) + { + _configService.Current.ShowUidInDtrTooltip = showUidInDtrTooltip; + _configService.Save(); + } + + if (ImGui.Checkbox("Prefer notes over player names in tooltip", ref preferNoteInDtrTooltip)) + { + _configService.Current.PreferNoteInDtrTooltip = preferNoteInDtrTooltip; + _configService.Save(); + } + + } + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + behaviorTree.MarkContentEnd(); + } } ImGui.Separator(); - var forceOpenLightfinder = _openLightfinderSectionOnNextDraw; - if (_openLightfinderSectionOnNextDraw) + using (var lightfinderTree = BeginGeneralTree("Lightfinder", UIColors.Get("LightlessPurple"))) { - ImGui.SetNextItemOpen(true, ImGuiCond.Always); - } - - if (_uiShared.MediumTreeNode("Lightfinder", UIColors.Get("LightlessPurple"))) - { - if (forceOpenLightfinder) + if (lightfinderTree.Visible) { - ImGui.SetScrollHereY(); - } - - _openLightfinderSectionOnNextDraw = false; - - bool autoEnable = _configService.Current.LightfinderAutoEnableOnConnect; - var autoAlign = _configService.Current.LightfinderAutoAlign; - var offsetX = (int)_configService.Current.LightfinderLabelOffsetX; + bool autoEnable = _configService.Current.LightfinderAutoEnableOnConnect; + var autoAlign = _configService.Current.LightfinderAutoAlign; + var offsetX = (int)_configService.Current.LightfinderLabelOffsetX; var offsetY = (int)_configService.Current.LightfinderLabelOffsetY; var labelScale = _configService.Current.LightfinderLabelScale; bool showLightfinderInDtr = _configService.Current.ShowLightfinderInDtr; @@ -1585,7 +2193,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } _uiShared.DrawHelpText("When enabled, Lightfinder will automatically turn on after reconnecting to the Lightless server."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Lightfinder Nameplate Colors"); if (ImGui.BeginTable("##LightfinderColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) @@ -1641,7 +2249,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Spacing(); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Lightfinder Info Bar"); if (ImGui.Checkbox("Show Lightfinder status in Server info bar", ref showLightfinderInDtr)) @@ -1737,7 +2345,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.EndDisabled(); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Alignment"); ImGui.BeginDisabled(autoAlign); @@ -1745,8 +2353,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LightfinderLabelOffsetX = (short)offsetX; _configService.Save(); - _nameplateHandler.ClearNameplateCaches(); - _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } @@ -1754,8 +2360,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LightfinderLabelOffsetX = 0; _configService.Save(); - _nameplateHandler.ClearNameplateCaches(); - _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } @@ -1770,8 +2374,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LightfinderLabelOffsetY = (short)offsetY; _configService.Save(); - _nameplateHandler.ClearNameplateCaches(); - _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } @@ -1779,8 +2381,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LightfinderLabelOffsetY = 0; _configService.Save(); - _nameplateHandler.ClearNameplateCaches(); - _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } @@ -1792,8 +2392,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LightfinderLabelScale = labelScale; _configService.Save(); - _nameplateHandler.ClearNameplateCaches(); - _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } @@ -1801,8 +2399,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LightfinderLabelScale = 1.0f; _configService.Save(); - _nameplateHandler.ClearNameplateCaches(); - _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } @@ -1816,8 +2412,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LightfinderAutoAlign = autoAlign; _configService.Save(); - _nameplateHandler.ClearNameplateCaches(); - _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } @@ -1849,7 +2443,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LabelAlignment = option; _configService.Save(); - _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } @@ -1862,7 +2455,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Visibility"); var showOwn = _configService.Current.LightfinderLabelShowOwn; @@ -1870,8 +2463,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LightfinderLabelShowOwn = showOwn; _configService.Save(); - _nameplateHandler.ClearNameplateCaches(); - _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } @@ -1882,8 +2473,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LightfinderLabelShowPaired = showPaired; _configService.Save(); - _nameplateHandler.ClearNameplateCaches(); - _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } @@ -1894,13 +2483,11 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LightfinderLabelShowHidden = showHidden; _configService.Save(); - _nameplateHandler.ClearNameplateCaches(); - _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); } _uiShared.DrawHelpText("Toggles Lightfinder label when no nameplate(s) is visible."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Label"); var useIcon = _configService.Current.LightfinderLabelUseIcon; @@ -1908,18 +2495,11 @@ public class SettingsUi : WindowMediatorSubscriberBase { _configService.Current.LightfinderLabelUseIcon = useIcon; _configService.Save(); - _nameplateHandler.ClearNameplateCaches(); - _nameplateHandler.FlagRefresh(); _nameplateService.RequestRedraw(); if (useIcon) { - RefreshLightfinderIconState(); - } - else - { - _lightfinderIconInputInitialized = false; - _lightfinderIconPresetIndex = -1; + // redo } } @@ -1927,445 +2507,553 @@ public class SettingsUi : WindowMediatorSubscriberBase if (useIcon) { - if (!_lightfinderIconInputInitialized) - { - RefreshLightfinderIconState(); - } - - var currentPresetLabel = _lightfinderIconPresetIndex >= 0 - ? $"{GetLightfinderPresetGlyph(_lightfinderIconPresetIndex)} {LightfinderIconPresets[_lightfinderIconPresetIndex].Label}" - : "Custom"; - - if (ImGui.BeginCombo("Preset Icon", currentPresetLabel)) - { - for (int i = 0; i < LightfinderIconPresets.Length; i++) - { - var optionGlyph = GetLightfinderPresetGlyph(i); - var preview = $"{optionGlyph} {LightfinderIconPresets[i].Label}"; - var selected = i == _lightfinderIconPresetIndex; - if (ImGui.Selectable(preview, selected)) - { - _lightfinderIconInput = NameplateHandler.ToIconEditorString(optionGlyph); - _lightfinderIconPresetIndex = i; - } - } - - if (ImGui.Selectable("Custom", _lightfinderIconPresetIndex == -1)) - { - _lightfinderIconPresetIndex = -1; - } - - ImGui.EndCombo(); - } - - var editorBuffer = _lightfinderIconInput; - if (ImGui.InputText("Icon Glyph", ref editorBuffer, 16)) - { - _lightfinderIconInput = editorBuffer; - _lightfinderIconPresetIndex = -1; - } - - if (ImGui.Button("Apply Icon")) - { - var normalized = NameplateHandler.NormalizeIconGlyph(_lightfinderIconInput); - ApplyLightfinderIcon(normalized, _lightfinderIconPresetIndex); - } - - ImGui.SameLine(); - if (ImGui.Button("Reset Icon")) - { - var defaultGlyph = NameplateHandler.NormalizeIconGlyph(null); - var defaultIndex = -1; - for (int i = 0; i < LightfinderIconPresets.Length; i++) - { - if (string.Equals(GetLightfinderPresetGlyph(i), defaultGlyph, StringComparison.Ordinal)) - { - defaultIndex = i; - break; - } - } - - if (defaultIndex < 0) - { - defaultIndex = 0; - } - - ApplyLightfinderIcon(GetLightfinderPresetGlyph(defaultIndex), defaultIndex); - } - - var previewGlyph = NameplateHandler.NormalizeIconGlyph(_lightfinderIconInput); - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.Text($"Preview: {previewGlyph}"); - _uiShared.DrawHelpText( - "Enter a hex code (e.g. E0BB), pick a preset, or paste an icon character directly."); - } - else - { - _lightfinderIconInputInitialized = false; - _lightfinderIconPresetIndex = -1; + //redo } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); + lightfinderTree.MarkContentEnd(); + } } ImGui.Separator(); - if (_uiShared.MediumTreeNode("Colors", UIColors.Get("LightlessPurple"))) + using (var pairListTree = BeginGeneralTree("Pair List", UIColors.Get("LightlessPurple"))) { - ImGui.TextUnformatted("UI Theme Colors"); - - var colorNames = new[] + if (pairListTree.Visible) { - ("LightlessPurple", "Primary Purple", "Section titles and dividers"), - ("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"), - ("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"), - ("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"), - ("LightlessGreen", "Success Green", "Join buttons and success messages"), - ("LightlessYellow", "Warning Yellow", "Warning colors"), - ("LightlessOrange", "Performance Orange", "Performance notifications and warnings"), - ("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"), - ("DimRed", "Error Red", "Error and offline colors") - }; - if (ImGui.BeginTable("##ColorTable", 3, - ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) - { - ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40); - ImGui.TableHeadersRow(); - - foreach (var (colorKey, displayName, description) in colorNames) + if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) { - ImGui.TableNextRow(); + _configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } - // color column - ImGui.TableSetColumnIndex(0); - var currentColor = UIColors.Get(colorKey); - var colorToEdit = currentColor; - if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, - ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) + _uiShared.DrawHelpText( + "This will show all currently visible users in a special 'Visible' group in the main UI."); + + using (ImRaii.Disabled(!showVisibleSeparate)) + { + using var indent = ImRaii.PushIndent(); + if (ImGui.Checkbox("Show Syncshell Users in Visible Group", ref groupInVisible)) { - UIColors.Set(colorKey, colorToEdit); + _configService.Current.ShowSyncshellUsersInVisible = groupInVisible; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); } + } - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(displayName); + if (ImGui.Checkbox("Show separate Offline group", ref showOfflineSeparate)) + { + _configService.Current.ShowOfflineUsersSeparately = showOfflineSeparate; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } - // description column - ImGui.TableSetColumnIndex(1); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(description); + _uiShared.DrawHelpText( + "This will show all currently offline users in a special 'Offline' group in the main UI."); - // actions column - ImGui.TableSetColumnIndex(2); - using var resetId = ImRaii.PushId($"Reset_{colorKey}"); - var availableWidth = ImGui.GetContentRegionAvail().X; - var isCustom = UIColors.IsCustom(colorKey); - - using (ImRaii.Disabled(!isCustom)) + using (ImRaii.Disabled(!showOfflineSeparate)) + { + using var indent = ImRaii.PushIndent(); + if (ImGui.Checkbox("Show separate Offline group for Syncshell users", ref syncshellOfflineSeparate)) { - using (ImRaii.PushFont(UiBuilder.IconFont)) + _configService.Current.ShowSyncshellOfflineUsersSeparately = syncshellOfflineSeparate; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + } + + if (ImGui.Checkbox("Group up all syncshells in one folder", ref groupUpSyncshells)) + { + _configService.Current.GroupUpSyncshells = groupUpSyncshells; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText( + "This will group up all Syncshells in a special 'All Syncshells' folder in the main UI."); + + if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells)) + { + _configService.Current.ShowGroupedSyncshellsInAll = groupedSyncshells; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText("This will show grouped syncshells in main screen or group 'All Syncshells'."); + + if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes)) + { + _configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText( + "This will show the character name instead of custom set note when a character is visible"); + + ImGui.Indent(); + if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.BeginDisabled(); + if (ImGui.Checkbox("Prefer notes over player names for visible players", ref preferNotesInsteadOfName)) + { + _configService.Current.PreferNotesOverNamesForVisible = preferNotesInsteadOfName; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText("If you set a note for a player it will be shown instead of the player name"); + if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.EndDisabled(); + ImGui.Unindent(); + + if (ImGui.Checkbox("Set visible pairs as focus targets when clicking the eye", ref useFocusTarget)) + { + _configService.Current.UseFocusTarget = useFocusTarget; + _configService.Save(); + } + + if (ImGui.Checkbox("Set visible pair icon to an green color", ref greenVisiblePair)) + { + _configService.Current.ShowVisiblePairsGreenEye = greenVisiblePair; + _configService.Save(); + } + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + pairListTree.MarkContentEnd(); + } + } + + ImGui.Separator(); + + using (var profilesTree = BeginGeneralTree("Profiles", UIColors.Get("LightlessPurple"))) + { + if (profilesTree.Visible) + { + if (ImGui.Checkbox("Show Lightless Profiles on Hover", ref showProfiles)) + { + Mediator.Publish(new ClearProfileUserDataMessage()); + _configService.Current.ProfilesShow = showProfiles; + _configService.Save(); + } + + _uiShared.DrawHelpText("This will show the configured user profile after a set delay"); + ImGui.Indent(); + if (!showProfiles) ImGui.BeginDisabled(); + if (ImGui.Checkbox("Popout profiles on the right", ref profileOnRight)) + { + _configService.Current.ProfilePopoutRight = profileOnRight; + _configService.Save(); + Mediator.Publish(new CompactUiChange(Vector2.Zero, Vector2.Zero)); + } + + _uiShared.DrawHelpText("Will show profiles on the right side of the main UI"); + if (ImGui.SliderFloat("Hover Delay", ref profileDelay, 1, 10)) + { + _configService.Current.ProfileDelay = profileDelay; + _configService.Save(); + } + + _uiShared.DrawHelpText("Delay until the profile should be displayed"); + if (!showProfiles) ImGui.EndDisabled(); + ImGui.Unindent(); + if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles)) + { + Mediator.Publish(new ClearProfileUserDataMessage()); + _configService.Current.ProfilesAllowNsfw = showNsfwProfiles; + _configService.Save(); + } + + _uiShared.DrawHelpText("Will show profiles that have the NSFW tag enabled"); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + profilesTree.MarkContentEnd(); + } + } + + ImGui.Separator(); + + ImGui.Dummy(new Vector2(10)); + _uiShared.BigText("UI Theme"); + + using (var colorsTree = BeginGeneralTree("Colors", UIColors.Get("LightlessPurple"))) + { + if (colorsTree.Visible) + { + ImGui.TextUnformatted("UI Theme Colors"); + + var colorNames = new[] + { + ("LightlessPurple", "Primary Purple", "Section titles and dividers"), + ("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"), + ("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"), + ("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"), + ("LightlessGreen", "Success Green", "Join buttons and success messages"), + ("LightlessYellow", "Warning Yellow", "Warning colors"), + ("LightlessOrange", "Performance Orange", "Performance notifications and warnings"), + ("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"), + ("DimRed", "Error Red", "Error and offline colors") + }; + if (ImGui.BeginTable("##ColorTable", 3, + ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) + { + ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40); + ImGui.TableHeadersRow(); + + foreach (var (colorKey, displayName, description) in colorNames) + { + ImGui.TableNextRow(); + + ImGui.TableSetColumnIndex(0); + var currentColor = UIColors.Get(colorKey); + var colorToEdit = currentColor; + if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, + ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) { - if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) + UIColors.Set(colorKey, colorToEdit); + } + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(displayName); + + ImGui.TableSetColumnIndex(1); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(description); + + ImGui.TableSetColumnIndex(2); + using var resetId = ImRaii.PushId($"Reset_{colorKey}"); + var availableWidth = ImGui.GetContentRegionAvail().X; + var isCustom = UIColors.IsCustom(colorKey); + + using (ImRaii.Disabled(!isCustom)) + { + using (ImRaii.PushFont(UiBuilder.IconFont)) { - UIColors.Reset(colorKey); + if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) + { + UIColors.Reset(colorKey); + } } } + + UiSharedService.AttachToolTip(isCustom + ? "Reset this color to default" + : "Color is already at default value"); } - UiSharedService.AttachToolTip(isCustom - ? "Reset this color to default" - : "Color is already at default value"); + ImGui.EndTable(); } - ImGui.EndTable(); - } - - ImGui.Spacing(); - if (_uiShared.IconTextButton(FontAwesomeIcon.Undo, "Reset All Theme Colors")) - { - UIColors.ResetAll(); - } - - _uiShared.DrawHelpText("This will reset all theme colors to their default values"); - - ImGui.Spacing(); - - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - - ImGui.TextUnformatted("Server Info Bar Colors"); - - if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr)) - { - _configService.Current.UseColorsInDtr = useColorsInDtr; - _configService.Save(); - } - - _uiShared.DrawHelpText( - "This will color the Server Info Bar entry based on connection status and visible pairs."); - - ImGui.BeginDisabled(!useColorsInDtr); - const ImGuiTableFlags serverInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit; - if (ImGui.BeginTable("##ServerInfoBarColorTable", 3, serverInfoTableFlags)) - { - ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthFixed, 220f); - ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); - ImGui.TableHeadersRow(); - - DrawDtrColorRow( - "server-default", - "Default", - "Displayed when connected without any special status.", - ref dtrColorsDefault, - DefaultConfig.DtrColorsDefault, - value => _configService.Current.DtrColorsDefault = value); - - DrawDtrColorRow( - "server-not-connected", - "Not Connected", - "Shown while disconnected from the Lightless server.", - ref dtrColorsNotConnected, - DefaultConfig.DtrColorsNotConnected, - value => _configService.Current.DtrColorsNotConnected = value); - - DrawDtrColorRow( - "server-pairs", - "Pairs in Range", - "Used when nearby paired players are detected.", - ref dtrColorsPairsInRange, - DefaultConfig.DtrColorsPairsInRange, - value => _configService.Current.DtrColorsPairsInRange = value); - - ImGui.EndTable(); - } - ImGui.EndDisabled(); - - ImGui.Spacing(); - - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - - ImGui.TextUnformatted("Nameplate Colors"); - - var nameColorsEnabled = _configService.Current.IsNameplateColorsEnabled; - var nameColors = _configService.Current.NameplateColors; - var isFriendOverride = _configService.Current.overrideFriendColor; - var isPartyOverride = _configService.Current.overridePartyColor; - var isFcTagOverride = _configService.Current.overrideFcTagColor; - - if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled)) - { - _configService.Current.IsNameplateColorsEnabled = nameColorsEnabled; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - - _uiShared.DrawHelpText("This will override the nameplate colors for visible paired players in-game."); - - using (ImRaii.Disabled(!nameColorsEnabled)) - { - using var indent = ImRaii.PushIndent(); - if (InputDtrColors("Name color", ref nameColors)) + ImGui.Spacing(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Undo, "Reset All Theme Colors")) { - _configService.Current.NameplateColors = nameColors; - _configService.Save(); - _nameplateService.RequestRedraw(); + UIColors.ResetAll(); } - if (ImGui.Checkbox("Override friend color", ref isFriendOverride)) + _uiShared.DrawHelpText("This will reset all theme colors to their default values"); + + ImGui.Spacing(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + + ImGui.TextUnformatted("UI Theme"); + + if (ImGui.Checkbox("Use the redesign of the UI for Lightless client", ref useLightlessRedesign)) { - _configService.Current.overrideFriendColor = isFriendOverride; + _configService.Current.UseLightlessRedesign = useLightlessRedesign; _configService.Save(); - _nameplateService.RequestRedraw(); } - if (ImGui.Checkbox("Override party color", ref isPartyOverride)) + var usePairColoredUIDs = _configService.Current.useColoredUIDs; + + if (ImGui.Checkbox("Toggle the colored UID's in pair list", ref usePairColoredUIDs)) { - _configService.Current.overridePartyColor = isPartyOverride; + _configService.Current.useColoredUIDs = usePairColoredUIDs; _configService.Save(); - _nameplateService.RequestRedraw(); } - if (ImGui.Checkbox("Override FC tag color", ref isFcTagOverride)) - { - _configService.Current.overrideFcTagColor = isFcTagOverride; - _configService.Save(); - _nameplateService.RequestRedraw(); - } + _uiShared.DrawHelpText("This changes the vanity colored UID's in pair list."); + + DrawThemeOverridesSection(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + colorsTree.MarkContentEnd(); } - - ImGui.Spacing(); - - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - - ImGui.TextUnformatted("UI Theme"); - - if (ImGui.Checkbox("Use the redesign of the UI for Lightless client", ref useLightlessRedesign)) - { - _configService.Current.UseLightlessRedesign = useLightlessRedesign; - _configService.Save(); - } - - var usePairColoredUIDs = _configService.Current.useColoredUIDs; - - if (ImGui.Checkbox("Toggle the colored UID's in pair list", ref usePairColoredUIDs)) - { - _configService.Current.useColoredUIDs = usePairColoredUIDs; - _configService.Save(); - } - - _uiShared.DrawHelpText("This changes the vanity colored UID's in pair list."); - - DrawThemeOverridesSection(); - - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); } ImGui.Separator(); - if (_uiShared.MediumTreeNode("Pair List", UIColors.Get("LightlessPurple"))) + using (var serverInfoTree = BeginGeneralTree("Server Info Bar", UIColors.Get("LightlessPurple"))) { - if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) + if (serverInfoTree.Visible) { - _configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } + ImGui.TextUnformatted("Server Info Bar Colors"); - _uiShared.DrawHelpText( - "This will show all currently visible users in a special 'Visible' group in the main UI."); - - using (ImRaii.Disabled(!showVisibleSeparate)) - { - using var indent = ImRaii.PushIndent(); - if (ImGui.Checkbox("Show Syncshell Users in Visible Group", ref groupInVisible)) + if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr)) { - _configService.Current.ShowSyncshellUsersInVisible = groupInVisible; + _configService.Current.UseColorsInDtr = useColorsInDtr; _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); } - } - if (ImGui.Checkbox("Show separate Offline group", ref showOfflineSeparate)) - { - _configService.Current.ShowOfflineUsersSeparately = showOfflineSeparate; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } + _uiShared.DrawHelpText( + "This will color the Server Info Bar entry based on connection status and visible pairs."); - _uiShared.DrawHelpText( - "This will show all currently offline users in a special 'Offline' group in the main UI."); - - using (ImRaii.Disabled(!showOfflineSeparate)) - { - using var indent = ImRaii.PushIndent(); - if (ImGui.Checkbox("Show separate Offline group for Syncshell users", ref syncshellOfflineSeparate)) + ImGui.BeginDisabled(!useColorsInDtr); + const ImGuiTableFlags serverInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit; + if (ImGui.BeginTable("##ServerInfoBarColorTable", 3, serverInfoTableFlags)) { - _configService.Current.ShowSyncshellOfflineUsersSeparately = syncshellOfflineSeparate; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); + ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthFixed, 220f); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); + ImGui.TableHeadersRow(); + + DrawDtrColorRow( + "server-default", + "Default", + "Displayed when connected without any special status.", + ref dtrColorsDefault, + DefaultConfig.DtrColorsDefault, + value => _configService.Current.DtrColorsDefault = value); + + DrawDtrColorRow( + "server-not-connected", + "Not Connected", + "Shown while disconnected from the Lightless server.", + ref dtrColorsNotConnected, + DefaultConfig.DtrColorsNotConnected, + value => _configService.Current.DtrColorsNotConnected = value); + + DrawDtrColorRow( + "server-pairs", + "Pairs in Range", + "Used when nearby paired players are detected.", + ref dtrColorsPairsInRange, + DefaultConfig.DtrColorsPairsInRange, + value => _configService.Current.DtrColorsPairsInRange = value); + + ImGui.EndTable(); } + ImGui.EndDisabled(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + serverInfoTree.MarkContentEnd(); } - - if (ImGui.Checkbox("Group up all syncshells in one folder", ref groupUpSyncshells)) - { - _configService.Current.GroupUpSyncshells = groupUpSyncshells; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText( - "This will group up all Syncshells in a special 'All Syncshells' folder in the main UI."); - - if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells)) - { - _configService.Current.ShowGroupedSyncshellsInAll = groupedSyncshells; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText("This will show grouped syncshells in main screen or group 'All Syncshells'."); - - if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes)) - { - _configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText( - "This will show the character name instead of custom set note when a character is visible"); - - ImGui.Indent(); - if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.BeginDisabled(); - if (ImGui.Checkbox("Prefer notes over player names for visible players", ref preferNotesInsteadOfName)) - { - _configService.Current.PreferNotesOverNamesForVisible = preferNotesInsteadOfName; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText("If you set a note for a player it will be shown instead of the player name"); - if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.EndDisabled(); - ImGui.Unindent(); - - if (ImGui.Checkbox("Set visible pairs as focus targets when clicking the eye", ref useFocusTarget)) - { - _configService.Current.UseFocusTarget = useFocusTarget; - _configService.Save(); - } - - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); } ImGui.Separator(); - if (_uiShared.MediumTreeNode("Profiles", UIColors.Get("LightlessPurple"))) + + using (var nameplateTree = BeginGeneralTree("Nameplate", UIColors.Get("LightlessPurple"))) { - if (ImGui.Checkbox("Show Lightless Profiles on Hover", ref showProfiles)) + if (nameplateTree.Visible) { - Mediator.Publish(new ClearProfileUserDataMessage()); - _configService.Current.ProfilesShow = showProfiles; - _configService.Save(); + ImGui.TextUnformatted("Nameplate Colors"); + + var nameColorsEnabled = _configService.Current.IsNameplateColorsEnabled; + var nameColors = _configService.Current.NameplateColors; + var isFriendOverride = _configService.Current.overrideFriendColor; + var isPartyOverride = _configService.Current.overridePartyColor; + + if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled)) + { + _configService.Current.IsNameplateColorsEnabled = nameColorsEnabled; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + _uiShared.DrawHelpText("This will override the nameplate colors for visible paired players in-game."); + + using (ImRaii.Disabled(!nameColorsEnabled)) + { + using var indent = ImRaii.PushIndent(); + if (InputDtrColors("Name color", ref nameColors)) + { + _configService.Current.NameplateColors = nameColors; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (ImGui.Checkbox("Override friend color", ref isFriendOverride)) + { + _configService.Current.overrideFriendColor = isFriendOverride; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (ImGui.Checkbox("Override party color", ref isPartyOverride)) + { + _configService.Current.overridePartyColor = isPartyOverride; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + } + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + nameplateTree.MarkContentEnd(); } - - _uiShared.DrawHelpText("This will show the configured user profile after a set delay"); - ImGui.Indent(); - if (!showProfiles) ImGui.BeginDisabled(); - if (ImGui.Checkbox("Popout profiles on the right", ref profileOnRight)) - { - _configService.Current.ProfilePopoutRight = profileOnRight; - _configService.Save(); - Mediator.Publish(new CompactUiChange(Vector2.Zero, Vector2.Zero)); - } - - _uiShared.DrawHelpText("Will show profiles on the right side of the main UI"); - if (ImGui.SliderFloat("Hover Delay", ref profileDelay, 1, 10)) - { - _configService.Current.ProfileDelay = profileDelay; - _configService.Save(); - } - - _uiShared.DrawHelpText("Delay until the profile should be displayed"); - if (!showProfiles) ImGui.EndDisabled(); - ImGui.Unindent(); - if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles)) - { - Mediator.Publish(new ClearProfileUserDataMessage()); - _configService.Current.ProfilesAllowNsfw = showNsfwProfiles; - _configService.Save(); - } - - _uiShared.DrawHelpText("Will show profiles that have the NSFW tag enabled"); - - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); } + ImGui.Separator(); + + ImGui.EndChild(); + ImGui.EndGroup(); + + generalSelune.DrawHighlightOnly(ImGui.GetIO().DeltaTime); + } + + private void DrawGeneralNavigation() + { + var buttonWidth = Math.Max(1f, ImGui.GetContentRegionAvail().X); + var buttonHeight = Math.Max(ImGui.GetFrameHeight(), 36f * ImGuiHelpers.GlobalScale); + for (var i = 0; i < _generalTreeNavOrder.Length; i++) + { + var label = _generalTreeNavOrder[i]; + using var id = ImRaii.PushId(label); + var isTarget = string.Equals(_generalOpenTreeTarget, label, StringComparison.Ordinal) || + string.Equals(_generalScrollTarget, label, StringComparison.Ordinal); + using var activeColor = isTarget + ? ImRaii.PushColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.TabActive]) + : null; + using var activeHover = isTarget + ? ImRaii.PushColor(ImGuiCol.ButtonHovered, ImGui.GetStyle().Colors[(int)ImGuiCol.TabHovered]) + : null; + using var activeActive = isTarget + ? ImRaii.PushColor(ImGuiCol.ButtonActive, ImGui.GetStyle().Colors[(int)ImGuiCol.TabActive]) + : null; + + if (ImGui.Button(label, new Vector2(buttonWidth, buttonHeight))) + { + FocusGeneralTree(label); + } + + if (_generalNavSeparatorAfter.Contains(label) && i < _generalTreeNavOrder.Length - 1) + { + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + } + } + } + + private GeneralTreeScope BeginGeneralTree(string label, Vector4 color) + { + var shouldForceOpen = string.Equals(_generalOpenTreeTarget, label, StringComparison.Ordinal); + if (shouldForceOpen) + { + ImGui.SetNextItemOpen(true, ImGuiCond.Always); + } + + var open = _uiShared.MediumTreeNode(label, color); + if (shouldForceOpen) + { + _generalOpenTreeTarget = null; + } + + var headerMin = ImGui.GetItemRectMin(); + var headerMax = ImGui.GetItemRectMax(); + var windowPos = ImGui.GetWindowPos(); + var contentRegionMin = windowPos + ImGui.GetWindowContentRegionMin(); + var contentRegionMax = windowPos + ImGui.GetWindowContentRegionMax(); + + if (open && string.Equals(_generalScrollTarget, label, StringComparison.Ordinal)) + { + ImGui.SetScrollHereY(0f); + _generalScrollTarget = null; + } + + return new GeneralTreeScope(open, color, GetGeneralTreeHighlightAlpha(label), headerMin, headerMax, contentRegionMin, contentRegionMax); + } + + private void FocusGeneralTree(string label) + { + _generalOpenTreeTarget = label; + _generalScrollTarget = label; + _generalTreeHighlights[label] = ImGui.GetTime(); + } + + private float GetGeneralTreeHighlightAlpha(string label) + { + if (!_generalTreeHighlights.TryGetValue(label, out var startTime)) + return 0f; + + var elapsed = (float)(ImGui.GetTime() - startTime); + if (elapsed >= GeneralTreeHighlightDuration) + { + _generalTreeHighlights.Remove(label); + return 0f; + } + + return 1f - (elapsed / GeneralTreeHighlightDuration); + } + + private struct GeneralTreeScope : IDisposable + { + private readonly bool _visible; + private readonly Vector4 _color; + private readonly float _highlightAlpha; + private readonly Vector2 _headerMin; + private readonly Vector2 _headerMax; + private readonly Vector2 _contentRegionMin; + private readonly Vector2 _contentRegionMax; + private Vector2 _contentEnd; + private bool _hasContentEnd; + + public bool Visible => _visible; + + public GeneralTreeScope(bool visible, Vector4 color, float highlightAlpha, Vector2 headerMin, Vector2 headerMax, Vector2 contentRegionMin, Vector2 contentRegionMax) + { + _visible = visible; + _color = color; + _highlightAlpha = highlightAlpha; + _headerMin = headerMin; + _headerMax = headerMax; + _contentRegionMin = contentRegionMin; + _contentRegionMax = contentRegionMax; + _contentEnd = Vector2.Zero; + _hasContentEnd = false; + } + + public void MarkContentEnd() + { + if (!_visible) + return; + + _contentEnd = ImGui.GetCursorScreenPos(); + _hasContentEnd = true; + } + + public void Dispose() + { + if (_highlightAlpha <= 0f) + return; + + var style = ImGui.GetStyle(); + var rectMin = new Vector2(_contentRegionMin.X, _headerMin.Y) - new Vector2(0f, 2f); + var rectMax = new Vector2(_contentRegionMax.X, _headerMax.Y) + new Vector2(0f, 2f); + + if (_visible) + { + var contentEnd = _hasContentEnd ? _contentEnd : ImGui.GetCursorScreenPos(); + rectMax.Y = Math.Max(rectMax.Y, contentEnd.Y + style.ItemSpacing.Y + 2f); + } + + Selune.RegisterHighlight( + rectMin, + rectMax, + SeluneHighlightMode.Both, + borderOnly: false, + exactSize: true, + clipToElement: true, + clipPadding: new Vector2(0f, 4f), + highlightColorOverride: new Vector4(_color.X, _color.Y, _color.Z, 0.4f), + highlightAlphaOverride: _highlightAlpha); + } } private void DrawPerformance() @@ -2388,6 +3076,22 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText( "Will show a performance indicator when players exceed defined thresholds in Lightless UI." + Environment.NewLine + "Will use warning thresholds."); + + using (ImRaii.Disabled(!showPerformanceIndicator)) + { + using var indent = ImRaii.PushIndent(); + bool showCompactStats = _playerPerformanceConfigService.Current.ShowPerformanceUsageNextToName; + if (ImGui.Checkbox("Show performance stats next to alias", ref showCompactStats)) + { + _playerPerformanceConfigService.Current.ShowPerformanceUsageNextToName = showCompactStats; + _playerPerformanceConfigService.Save(); + } + + _uiShared.DrawHelpText( + "Adds a text with approx. VRAM usage and triangle count to the right of pairs alias." + + Environment.NewLine + "Requires performance indicator to be enabled."); + } + bool warnOnExceedingThresholds = _playerPerformanceConfigService.Current.WarnOnExceedingThresholds; if (ImGui.Checkbox("Warn on loading in players exceeding performance thresholds", ref warnOnExceedingThresholds)) @@ -2444,7 +3148,7 @@ public class SettingsUi : WindowMediatorSubscriberBase + "Default: 165 thousand"); } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -2548,7 +3252,103 @@ public class SettingsUi : WindowMediatorSubscriberBase + "Default: 250 thousand"); } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + } + + ImGui.Separator(); + + if (_uiShared.MediumTreeNode("Texture Optimization", UIColors.Get("LightlessYellow"))) + { + _uiShared.MediumText("Warning", UIColors.Get("DimRed")); + _uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"), + new SeStringUtils.RichTextEntry("Texture compression and downscaling is potentially a "), + new SeStringUtils.RichTextEntry("destructive", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(" process and may cause broken or incorrect character appearances.")); + + _uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"), + new SeStringUtils.RichTextEntry("This feature is encouraged to help "), + new SeStringUtils.RichTextEntry("lower-end systems with limited VRAM", UIColors.Get("LightlessYellow"), true), + new SeStringUtils.RichTextEntry(" and for use in "), + new SeStringUtils.RichTextEntry("performance-critical scenarios", UIColors.Get("LightlessYellow"), true), + new SeStringUtils.RichTextEntry(".")); + + _uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"), + new SeStringUtils.RichTextEntry("Runtime downscaling "), + new SeStringUtils.RichTextEntry("MAY", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(" cause higher load on the system when processing downloads.")); + + _uiShared.DrawNoteLine("!!! ", UIColors.Get("DimRed"), + new SeStringUtils.RichTextEntry("When enabled, we cannot provide support for appearance issues caused by this setting!", UIColors.Get("DimRed"), true)); + + var textureConfig = _playerPerformanceConfigService.Current; + var trimNonIndex = textureConfig.EnableNonIndexTextureMipTrim; + if (ImGui.Checkbox("Trim mip levels for textures", ref trimNonIndex)) + { + textureConfig.EnableNonIndexTextureMipTrim = trimNonIndex; + _playerPerformanceConfigService.Save(); + } + _uiShared.DrawHelpText("When enabled, Lightless will remove high-resolution mip levels from textures (not index) that exceed the size limit and are not compressed with any kind compression."); + + var downscaleIndex = textureConfig.EnableIndexTextureDownscale; + if (ImGui.Checkbox("Downscale index textures above limit", ref downscaleIndex)) + { + textureConfig.EnableIndexTextureDownscale = downscaleIndex; + _playerPerformanceConfigService.Save(); + } + _uiShared.DrawHelpText("Controls whether Lightless reduces index textures that exceed the size limit."); + + var dimensionOptions = new[] { 512, 1024, 2048, 4096 }; + var optionLabels = dimensionOptions.Select(static value => value.ToString()).ToArray(); + var currentDimension = textureConfig.TextureDownscaleMaxDimension; + var selectedIndex = Array.IndexOf(dimensionOptions, currentDimension); + if (selectedIndex < 0) + { + selectedIndex = Array.IndexOf(dimensionOptions, 2048); + } + + ImGui.SetNextItemWidth(140 * ImGuiHelpers.GlobalScale); + if (ImGui.Combo("Maximum texture dimension", ref selectedIndex, optionLabels, optionLabels.Length)) + { + textureConfig.TextureDownscaleMaxDimension = dimensionOptions[selectedIndex]; + _playerPerformanceConfigService.Save(); + } + _uiShared.DrawHelpText($"Textures above this size will be reduced until their largest dimension is at or below the limit. Block-compressed textures are skipped when \"Only downscale uncompressed\" is enabled.{UiSharedService.TooltipSeparator}Default: 2048"); + + var keepOriginalTextures = textureConfig.KeepOriginalTextureFiles; + if (ImGui.Checkbox("Keep original texture files", ref keepOriginalTextures)) + { + textureConfig.KeepOriginalTextureFiles = keepOriginalTextures; + _playerPerformanceConfigService.Save(); + } + _uiShared.DrawHelpText("When disabled, Lightless removes the original texture after a downscaled copy is created."); + ImGui.SameLine(); + _uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective VRAM usage information will not work.", UIColors.Get("LightlessYellow"))); + + if (!textureConfig.EnableNonIndexTextureMipTrim && !textureConfig.EnableIndexTextureDownscale) + { + UiSharedService.ColorTextWrapped("Both trimming and downscale are disabled. Lightless will keep original textures regardless of size.", UIColors.Get("DimRed")); + } + + ImGui.Dummy(new Vector2(5)); + + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 3f); + var onlyUncompressed = textureConfig.OnlyDownscaleUncompressedTextures; + if (ImGui.Checkbox("Only downscale uncompressed textures", ref onlyUncompressed)) + { + textureConfig.OnlyDownscaleUncompressedTextures = onlyUncompressed; + _playerPerformanceConfigService.Save(); + } + _uiShared.DrawHelpText("If disabled, compressed textures will be targeted for downscaling too."); + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 3f); + + ImGui.Dummy(new Vector2(5)); + + DrawTextureDownscaleCounters(); + + ImGui.Dummy(new Vector2(5)); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); ImGui.TreePop(); } @@ -2696,7 +3496,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndPopup(); } - _uiShared.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); ImGui.TreePop(); } @@ -2748,541 +3548,564 @@ public class SettingsUi : WindowMediatorSubscriberBase bool useOauth = selectedServer.UseOAuth2; - if (ImGui.BeginTabBar("serverTabBar")) + _serverTabOptions.Clear(); + _serverTabOptions.Add(new UiSharedService.TabOption( + "Character Management", + ServerSettingsTab.CharacterManagement)); + + if (!useOauth) { - if (ImGui.BeginTabItem("Character Management")) - { - if (selectedServer.SecretKeys.Any() || useOauth) - { - UiSharedService.ColorTextWrapped( - "Characters listed here will automatically connect to the selected Lightless service with the settings as provided below." + - " Make sure to enter the character names correctly or use the 'Add current character' button at the bottom.", - UIColors.Get("LightlessYellow")); - int i = 0; - _uiShared.DrawUpdateOAuthUIDsButton(selectedServer); + _serverTabOptions.Add(new UiSharedService.TabOption( + "Secret Key Management", + ServerSettingsTab.SecretKeyManagement)); + } - if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken)) - { - bool hasSetSecretKeysButNoUid = - selectedServer.Authentications.Exists(u => - u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID)); - if (hasSetSecretKeysButNoUid) - { - ImGui.Dummy(new(5f, 5f)); - UiSharedService.TextWrapped( - "Some entries have been detected that have previously been assigned secret keys but not UIDs. " + - "Press this button below to attempt to convert those entries."); - using (ImRaii.Disabled(_secretKeysConversionTask != null && - !_secretKeysConversionTask.IsCompleted)) - { - if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowsLeftRight, - "Try to Convert Secret Keys to UIDs")) - { - _secretKeysConversionTask = - ConvertSecretKeysToUIDs(selectedServer, _secretKeysConversionCts.Token); - } - } + _serverTabOptions.Add(new UiSharedService.TabOption( + "Service Configuration", + ServerSettingsTab.ServiceConfiguration)); + _serverTabOptions.Add(new UiSharedService.TabOption( + "Permission Settings", + ServerSettingsTab.PermissionSettings)); - if (_secretKeysConversionTask != null && !_secretKeysConversionTask.IsCompleted) - { - UiSharedService.ColorTextWrapped("Converting Secret Keys to UIDs", - UIColors.Get("LightlessYellow")); - } + UiSharedService.Tab("ServerSettingsTabs", _serverTabOptions, ref _selectedServerTab); + ImGuiHelpers.ScaledDummy(5); - if (_secretKeysConversionTask != null && _secretKeysConversionTask.IsCompletedSuccessfully) - { - Vector4? textColor = null; - if (_secretKeysConversionTask.Result.PartialSuccess) - { - textColor = UIColors.Get("LightlessYellow"); - } - - if (!_secretKeysConversionTask.Result.Success) - { - textColor = ImGuiColors.DalamudRed; - } - - string text = $"Conversion has completed: {_secretKeysConversionTask.Result.Result}"; - if (textColor == null) - { - UiSharedService.TextWrapped(text); - } - else - { - UiSharedService.ColorTextWrapped(text, textColor!.Value); - } - - if (!_secretKeysConversionTask.Result.Success || - _secretKeysConversionTask.Result.PartialSuccess) - { - UiSharedService.TextWrapped( - "In case of conversion failures, please set the UIDs for the failed conversions manually."); - } - } - } - } - - ImGui.Separator(); - string youName = _dalamudUtilService.GetPlayerName(); - uint youWorld = _dalamudUtilService.GetHomeWorldId(); - ulong youCid = _dalamudUtilService.GetCID(); - if (!selectedServer.Authentications.Exists(a => - string.Equals(a.CharacterName, youName, StringComparison.Ordinal) && a.WorldId == youWorld)) - { - _uiShared.BigText("Your Character is not Configured", ImGuiColors.DalamudRed); - UiSharedService.ColorTextWrapped( - "You have currently no character configured that corresponds to your current name and world.", - ImGuiColors.DalamudRed); - var authWithCid = selectedServer.Authentications.Find(f => f.LastSeenCID == youCid); - if (authWithCid != null) - { - ImGuiHelpers.ScaledDummy(5); - UiSharedService.ColorText( - "A potential rename/world change from this character was detected:", - UIColors.Get("LightlessYellow")); - using (ImRaii.PushIndent(10f)) - UiSharedService.ColorText( - "Entry: " + authWithCid.CharacterName + " - " + - _dalamudUtilService.WorldData.Value[(ushort)authWithCid.WorldId], - UIColors.Get("LightlessBlue")); - UiSharedService.ColorText( - "Press the button below to adjust that entry to your current character:", - UIColors.Get("LightlessYellow")); - using (ImRaii.PushIndent(10f)) - UiSharedService.ColorText( - "Current: " + youName + " - " + - _dalamudUtilService.WorldData.Value[(ushort)youWorld], - UIColors.Get("LightlessBlue")); - ImGuiHelpers.ScaledDummy(5); - if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowRight, - "Update Entry to Current Character")) - { - authWithCid.CharacterName = youName; - authWithCid.WorldId = youWorld; - _serverConfigurationManager.Save(); - } - } - - ImGuiHelpers.ScaledDummy(5); - ImGui.Separator(); - ImGuiHelpers.ScaledDummy(5); - } - - foreach (var item in selectedServer.Authentications.ToList()) - { - using var charaId = ImRaii.PushId("selectedChara" + i); - - var worldIdx = (ushort)item.WorldId; - var data = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal) - .ToDictionary(k => k.Key, k => k.Value); - if (!data.TryGetValue(worldIdx, out string? worldPreview)) - { - worldPreview = data.First().Value; - } - - Dictionary keys = []; - - if (!useOauth) - { - var secretKeyIdx = item.SecretKeyIdx; - keys = selectedServer.SecretKeys; - if (!keys.TryGetValue(secretKeyIdx, out var secretKey)) - { - secretKey = new(); - } - } - - bool thisIsYou = false; - if (string.Equals(youName, item.CharacterName, StringComparison.OrdinalIgnoreCase) - && youWorld == worldIdx) - { - thisIsYou = true; - } - - bool misManaged = false; - if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken) && - string.IsNullOrEmpty(item.UID)) - { - misManaged = true; - } - - if (!selectedServer.UseOAuth2 && item.SecretKeyIdx == -1) - { - misManaged = true; - } - - Vector4 color = UIColors.Get("LightlessBlue"); - string text = thisIsYou ? "Your Current Character" : string.Empty; - if (misManaged) - { - text += " [MISMANAGED (" + (selectedServer.UseOAuth2 ? "No UID Set" : "No Secret Key Set") + - ")]"; - color = ImGuiColors.DalamudRed; - } - - if (selectedServer.Authentications.Where(e => e != item).Any(e => - string.Equals(e.CharacterName, item.CharacterName, StringComparison.Ordinal) - && e.WorldId == item.WorldId)) - { - text += " [DUPLICATE]"; - color = ImGuiColors.DalamudRed; - } - - if (!string.IsNullOrEmpty(text)) - { - text = text.Trim(); - _uiShared.BigText(text, color); - } - - var charaName = item.CharacterName; - if (ImGui.InputText("Character Name", ref charaName, 64)) - { - item.CharacterName = charaName; - _serverConfigurationManager.Save(); - } - - _uiShared.DrawCombo("World##" + item.CharacterName + i, data, (w) => w.Value, - (w) => - { - if (item.WorldId != w.Key) - { - item.WorldId = w.Key; - _serverConfigurationManager.Save(); - } - }, - EqualityComparer>.Default.Equals( - data.FirstOrDefault(f => f.Key == worldIdx), default) - ? data.First() - : data.First(f => f.Key == worldIdx)); - - if (!useOauth) - { - _uiShared.DrawCombo("Secret Key###" + item.CharacterName + i, keys, - (w) => w.Value.FriendlyName, - (w) => - { - if (w.Key != item.SecretKeyIdx) - { - item.SecretKeyIdx = w.Key; - _serverConfigurationManager.Save(); - } - }, - EqualityComparer>.Default.Equals( - keys.FirstOrDefault(f => f.Key == item.SecretKeyIdx), default) - ? keys.First() - : keys.First(f => f.Key == item.SecretKeyIdx)); - } - else - { - _uiShared.DrawUIDComboForAuthentication(i, item, selectedServer.ServerUri, _logger); - } - - bool isAutoLogin = item.AutoLogin; - if (ImGui.Checkbox("Automatically login to Lightless", ref isAutoLogin)) - { - item.AutoLogin = isAutoLogin; - _serverConfigurationManager.Save(); - } - - _uiShared.DrawHelpText( - "When enabled and logging into this character in XIV, Lightless will automatically connect to the current service."); - if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Character") && - UiSharedService.CtrlPressed()) - _serverConfigurationManager.RemoveCharacterFromServer(idx, item); - UiSharedService.AttachToolTip("Hold CTRL to delete this entry."); - - i++; - if (item != selectedServer.Authentications.ToList()[^1]) - { - ImGuiHelpers.ScaledDummy(5); - ImGui.Separator(); - ImGuiHelpers.ScaledDummy(5); - } - } - - if (selectedServer.Authentications.Any()) - ImGui.Separator(); - - if (!selectedServer.Authentications.Exists(c => - string.Equals(c.CharacterName, youName, StringComparison.Ordinal) - && c.WorldId == youWorld)) - { - if (_uiShared.IconTextButton(FontAwesomeIcon.User, "Add current character")) - { - _serverConfigurationManager.AddCurrentCharacterToServer(idx); - } - - ImGui.SameLine(); - } - - if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new character")) - { - _serverConfigurationManager.AddEmptyCharacterToServer(idx); - } - } - else - { - UiSharedService.ColorTextWrapped("You need to add a Secret Key first before adding Characters.", - UIColors.Get("LightlessYellow")); - } - - ImGui.EndTabItem(); - } - - if (!useOauth && ImGui.BeginTabItem("Secret Key Management")) - { - foreach (var item in selectedServer.SecretKeys.ToList()) - { - using var id = ImRaii.PushId("key" + item.Key); - var friendlyName = item.Value.FriendlyName; - if (ImGui.InputText("Secret Key Display Name", ref friendlyName, 255)) - { - item.Value.FriendlyName = friendlyName; - _serverConfigurationManager.Save(); - } - - var key = item.Value.Key; - if (ImGui.InputText("Secret Key", ref key, 64)) - { - item.Value.Key = key; - _serverConfigurationManager.Save(); - } - - if (!selectedServer.Authentications.Exists(p => p.SecretKeyIdx == item.Key)) - { - if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && - UiSharedService.CtrlPressed()) - { - selectedServer.SecretKeys.Remove(item.Key); - _serverConfigurationManager.Save(); - } - - UiSharedService.AttachToolTip("Hold CTRL to delete this secret key entry"); - } - else - { - UiSharedService.ColorTextWrapped("This key is in use and cannot be deleted", - UIColors.Get("LightlessYellow")); - } - - if (item.Key != selectedServer.SecretKeys.Keys.LastOrDefault()) - ImGui.Separator(); - } - - ImGui.Separator(); - if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new Secret Key")) - { - selectedServer.SecretKeys.Add( - selectedServer.SecretKeys.Any() ? selectedServer.SecretKeys.Max(p => p.Key) + 1 : 0, - new SecretKey() { FriendlyName = "New Secret Key", }); - _serverConfigurationManager.Save(); - } - - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Service Configuration")) - { - var serverName = selectedServer.ServerName; - var serverUri = selectedServer.ServerUri; - var isMain = string.Equals(serverName, ApiController.MainServer, StringComparison.OrdinalIgnoreCase); - var flags = isMain ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None; - - if (ImGui.InputText("Service URI", ref serverUri, 255, flags)) - { - selectedServer.ServerUri = serverUri; - } - - if (isMain) - { - _uiShared.DrawHelpText("You cannot edit the URI of the main service."); - } - - if (ImGui.InputText("Service Name", ref serverName, 255, flags)) - { - selectedServer.ServerName = serverName; - _serverConfigurationManager.Save(); - } - - if (isMain) - { - _uiShared.DrawHelpText("You cannot edit the name of the main service."); - } - - ImGui.SetNextItemWidth(200); - var serverTransport = _serverConfigurationManager.GetTransport(); - _uiShared.DrawCombo("Server Transport Type", - Enum.GetValues().Where(t => t != HttpTransportType.None), - (v) => v.ToString(), - onSelected: (t) => _serverConfigurationManager.SetTransportType(t), - serverTransport); - _uiShared.DrawHelpText( - "You normally do not need to change this, if you don't know what this is or what it's for, keep it to WebSockets." + - Environment.NewLine - + "If you run into connection issues with e.g. VPNs, try ServerSentEvents first before trying out LongPolling." + - UiSharedService.TooltipSeparator - + "Note: if the server does not support a specific Transport Type it will fall through to the next automatically: WebSockets > ServerSentEvents > LongPolling"); - - ImGuiHelpers.ScaledDummy(5); - - if (ImGui.Checkbox("Use Discord OAuth2 Authentication", ref useOauth)) - { - selectedServer.UseOAuth2 = useOauth; - _serverConfigurationManager.Save(); - } - - _uiShared.DrawHelpText( - "Use Discord OAuth2 Authentication to identify with this server instead of secret keys"); - if (useOauth) - { - _uiShared.DrawOAuth(selectedServer); - if (string.IsNullOrEmpty(_serverConfigurationManager.GetDiscordUserFromToken(selectedServer))) - { - ImGuiHelpers.ScaledDummy(10f); - UiSharedService.ColorTextWrapped( - "You have enabled OAuth2 but it is not linked. Press the buttons Check, then Authenticate to link properly.", - ImGuiColors.DalamudRed); - } - - if (!string.IsNullOrEmpty(_serverConfigurationManager.GetDiscordUserFromToken(selectedServer)) - && selectedServer.Authentications.TrueForAll(u => string.IsNullOrEmpty(u.UID))) - { - ImGuiHelpers.ScaledDummy(10f); - UiSharedService.ColorTextWrapped( - "You have enabled OAuth2 but no characters configured. Set the correct UIDs for your characters in \"Character Management\".", - ImGuiColors.DalamudRed); - } - } - - if (!isMain && selectedServer != _serverConfigurationManager.CurrentServer) - { - ImGui.Separator(); - if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Service") && - UiSharedService.CtrlPressed()) - { - _serverConfigurationManager.DeleteServer(selectedServer); - } - - _uiShared.DrawHelpText("Hold CTRL to delete this service"); - } - - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Permission Settings")) - { - _uiShared.BigText("Default Permission Settings"); - if (selectedServer == _serverConfigurationManager.CurrentServer && _apiController.IsConnected) - { - UiSharedService.TextWrapped( - "Note: The default permissions settings here are not applied retroactively to existing pairs or joined Syncshells."); - UiSharedService.TextWrapped( - "Note: The default permissions settings here are sent and stored on the connected service."); - ImGuiHelpers.ScaledDummy(5f); - var perms = _apiController.DefaultPermissions!; - bool individualIsSticky = perms.IndividualIsSticky; - bool disableIndividualSounds = perms.DisableIndividualSounds; - bool disableIndividualAnimations = perms.DisableIndividualAnimations; - bool disableIndividualVFX = perms.DisableIndividualVFX; - if (ImGui.Checkbox("Individually set permissions become preferred permissions", - ref individualIsSticky)) - { - perms.IndividualIsSticky = individualIsSticky; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText( - "The preferred attribute means that the permissions to that user will never change through any of your permission changes to Syncshells " + - "(i.e. if you have paused one specific user in a Syncshell and they become preferred permissions, then pause and unpause the same Syncshell, the user will remain paused - " + - "if a user does not have preferred permissions, it will follow the permissions of the Syncshell and be unpaused)." + - Environment.NewLine + Environment.NewLine + - "This setting means:" + Environment.NewLine + - " - All new individual pairs get their permissions defaulted to preferred permissions." + - Environment.NewLine + - " - All individually set permissions for any pair will also automatically become preferred permissions. This includes pairs in Syncshells." + - Environment.NewLine + Environment.NewLine + - "It is possible to remove or set the preferred permission state for any pair at any time." + - Environment.NewLine + Environment.NewLine + - "If unsure, leave this setting off."); - ImGuiHelpers.ScaledDummy(3f); - - if (ImGui.Checkbox("Disable individual pair sounds", ref disableIndividualSounds)) - { - perms.DisableIndividualSounds = disableIndividualSounds; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText("This setting will disable sound sync for all new individual pairs."); - if (ImGui.Checkbox("Disable individual pair animations", ref disableIndividualAnimations)) - { - perms.DisableIndividualAnimations = disableIndividualAnimations; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText("This setting will disable animation sync for all new individual pairs."); - if (ImGui.Checkbox("Disable individual pair VFX", ref disableIndividualVFX)) - { - perms.DisableIndividualVFX = disableIndividualVFX; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText("This setting will disable VFX sync for all new individual pairs."); - ImGuiHelpers.ScaledDummy(5f); - bool disableGroundSounds = perms.DisableGroupSounds; - bool disableGroupAnimations = perms.DisableGroupAnimations; - bool disableGroupVFX = perms.DisableGroupVFX; - if (ImGui.Checkbox("Disable Syncshell pair sounds", ref disableGroundSounds)) - { - perms.DisableGroupSounds = disableGroundSounds; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText( - "This setting will disable sound sync for all non-sticky pairs in newly joined syncshells."); - if (ImGui.Checkbox("Disable Syncshell pair animations", ref disableGroupAnimations)) - { - perms.DisableGroupAnimations = disableGroupAnimations; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText( - "This setting will disable animation sync for all non-sticky pairs in newly joined syncshells."); - if (ImGui.Checkbox("Disable Syncshell pair VFX", ref disableGroupVFX)) - { - perms.DisableGroupVFX = disableGroupVFX; - _ = _apiController.UserUpdateDefaultPermissions(perms); - } - - _uiShared.DrawHelpText( - "This setting will disable VFX sync for all non-sticky pairs in newly joined syncshells."); - } - else - { - UiSharedService.ColorTextWrapped("Default Permission Settings unavailable for this service. " + - "You need to connect to this service to change the default permissions since they are stored on the service.", - UIColors.Get("LightlessYellow")); - } - - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); + switch (_selectedServerTab) + { + case ServerSettingsTab.CharacterManagement: + DrawServerCharacterManagement(selectedServer, idx, useOauth); + break; + case ServerSettingsTab.SecretKeyManagement when !useOauth: + DrawServerSecretKeyManagement(selectedServer); + break; + case ServerSettingsTab.ServiceConfiguration: + DrawServerServiceConfiguration(selectedServer, ref useOauth); + break; + case ServerSettingsTab.PermissionSettings: + DrawServerPermissionSettings(selectedServer); + break; } ImGui.Dummy(new Vector2(10)); } + private void DrawServerCharacterManagement(ServerStorage selectedServer, int idx, bool useOauth) + { + if (selectedServer.SecretKeys.Any() || useOauth) + { + UiSharedService.ColorTextWrapped( + "Characters listed here will automatically connect to the selected Lightless service with the settings as provided below." + + " Make sure to enter the character names correctly or use the 'Add current character' button at the bottom.", + UIColors.Get("LightlessYellow")); + int i = 0; + _uiShared.DrawUpdateOAuthUIDsButton(selectedServer); + + if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken)) + { + bool hasSetSecretKeysButNoUid = + selectedServer.Authentications.Exists(u => + u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID)); + if (hasSetSecretKeysButNoUid) + { + ImGui.Dummy(new(5f, 5f)); + UiSharedService.TextWrapped( + "Some entries have been detected that have previously been assigned secret keys but not UIDs. " + + "Press this button below to attempt to convert those entries."); + using (ImRaii.Disabled(_secretKeysConversionTask != null && + !_secretKeysConversionTask.IsCompleted)) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowsLeftRight, + "Try to Convert Secret Keys to UIDs")) + { + _secretKeysConversionTask = + ConvertSecretKeysToUIDs(selectedServer, _secretKeysConversionCts.Token); + } + } + + if (_secretKeysConversionTask != null && !_secretKeysConversionTask.IsCompleted) + { + UiSharedService.ColorTextWrapped("Converting Secret Keys to UIDs", + UIColors.Get("LightlessYellow")); + } + + if (_secretKeysConversionTask != null && _secretKeysConversionTask.IsCompletedSuccessfully) + { + Vector4? textColor = null; + if (_secretKeysConversionTask.Result.PartialSuccess) + { + textColor = UIColors.Get("LightlessYellow"); + } + + if (!_secretKeysConversionTask.Result.Success) + { + textColor = ImGuiColors.DalamudRed; + } + + string text = $"Conversion has completed: {_secretKeysConversionTask.Result.Result}"; + if (textColor == null) + { + UiSharedService.TextWrapped(text); + } + else + { + UiSharedService.ColorTextWrapped(text, textColor!.Value); + } + + if (!_secretKeysConversionTask.Result.Success || + _secretKeysConversionTask.Result.PartialSuccess) + { + UiSharedService.TextWrapped( + "In case of conversion failures, please set the UIDs for the failed conversions manually."); + } + } + } + } + + ImGui.Separator(); + string youName = _dalamudUtilService.GetPlayerName(); + uint youWorld = _dalamudUtilService.GetHomeWorldId(); + ulong youCid = _dalamudUtilService.GetCID(); + if (!selectedServer.Authentications.Exists(a => + string.Equals(a.CharacterName, youName, StringComparison.Ordinal) && a.WorldId == youWorld)) + { + _uiShared.BigText("Your Character is not Configured", ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped( + "You have currently no character configured that corresponds to your current name and world.", + ImGuiColors.DalamudRed); + var authWithCid = selectedServer.Authentications.Find(f => f.LastSeenCID == youCid); + if (authWithCid != null) + { + ImGuiHelpers.ScaledDummy(5); + UiSharedService.ColorTextWrapped( + "A potential rename/world change from this character was detected:", + UIColors.Get("LightlessYellow")); + using (ImRaii.PushIndent(10f)) + UiSharedService.ColorText( + "Entry: " + authWithCid.CharacterName + " - " + + _dalamudUtilService.WorldData.Value[(ushort)authWithCid.WorldId], + UIColors.Get("LightlessBlue")); + UiSharedService.ColorText( + "Press the button below to adjust that entry to your current character:", + UIColors.Get("LightlessYellow")); + using (ImRaii.PushIndent(10f)) + UiSharedService.ColorText( + "Current: " + youName + " - " + + _dalamudUtilService.WorldData.Value[(ushort)youWorld], + UIColors.Get("LightlessBlue")); + ImGuiHelpers.ScaledDummy(5); + if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowRight, + "Update Entry to Current Character")) + { + authWithCid.CharacterName = youName; + authWithCid.WorldId = youWorld; + _serverConfigurationManager.Save(); + } + } + + ImGuiHelpers.ScaledDummy(5); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(5); + } + + foreach (var item in selectedServer.Authentications.ToList()) + { + using var charaId = ImRaii.PushId("selectedChara" + i); + + var worldIdx = (ushort)item.WorldId; + var data = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal) + .ToDictionary(k => k.Key, k => k.Value); + if (!data.TryGetValue(worldIdx, out string? worldPreview)) + { + worldPreview = data.First().Value; + } + + Dictionary keys = []; + + if (!useOauth) + { + var secretKeyIdx = item.SecretKeyIdx; + keys = selectedServer.SecretKeys; + if (!keys.TryGetValue(secretKeyIdx, out var secretKey)) + { + secretKey = new(); + } + } + + bool thisIsYou = false; + if (string.Equals(youName, item.CharacterName, StringComparison.OrdinalIgnoreCase) + && youWorld == worldIdx) + { + thisIsYou = true; + } + + bool misManaged = false; + if (selectedServer.UseOAuth2 && !string.IsNullOrEmpty(selectedServer.OAuthToken) && + string.IsNullOrEmpty(item.UID)) + { + misManaged = true; + } + + if (!selectedServer.UseOAuth2 && item.SecretKeyIdx == -1) + { + misManaged = true; + } + + Vector4 color = UIColors.Get("LightlessBlue"); + string text = thisIsYou ? "Your Current Character" : string.Empty; + if (misManaged) + { + text += " [MISMANAGED (" + (selectedServer.UseOAuth2 ? "No UID Set" : "No Secret Key Set") + + ")]"; + color = ImGuiColors.DalamudRed; + } + + if (selectedServer.Authentications.Where(e => e != item).Any(e => + string.Equals(e.CharacterName, item.CharacterName, StringComparison.Ordinal) + && e.WorldId == item.WorldId)) + { + text += " [DUPLICATE]"; + color = ImGuiColors.DalamudRed; + } + + if (!string.IsNullOrEmpty(text)) + { + text = text.Trim(); + _uiShared.BigText(text, color); + } + + var charaName = item.CharacterName; + if (ImGui.InputText("Character Name", ref charaName, 64)) + { + item.CharacterName = charaName; + _serverConfigurationManager.Save(); + } + + _uiShared.DrawCombo("World##" + item.CharacterName + i, data, (w) => w.Value, + (w) => + { + if (item.WorldId != w.Key) + { + item.WorldId = w.Key; + _serverConfigurationManager.Save(); + } + }, + EqualityComparer>.Default.Equals( + data.FirstOrDefault(f => f.Key == worldIdx), default) + ? data.First() + : data.First(f => f.Key == worldIdx)); + + if (!useOauth) + { + _uiShared.DrawCombo("Secret Key###" + item.CharacterName + i, keys, + (w) => w.Value.FriendlyName, + (w) => + { + if (w.Key != item.SecretKeyIdx) + { + item.SecretKeyIdx = w.Key; + _serverConfigurationManager.Save(); + } + }, + EqualityComparer>.Default.Equals( + keys.FirstOrDefault(f => f.Key == item.SecretKeyIdx), default) + ? keys.First() + : keys.First(f => f.Key == item.SecretKeyIdx)); + } + else + { + _uiShared.DrawUIDComboForAuthentication(i, item, selectedServer.ServerUri, _logger); + } + + bool isAutoLogin = item.AutoLogin; + if (ImGui.Checkbox("Automatically login to Lightless", ref isAutoLogin)) + { + item.AutoLogin = isAutoLogin; + _serverConfigurationManager.Save(); + } + + _uiShared.DrawHelpText( + "When enabled and logging into this character in XIV, Lightless will automatically connect to the current service."); + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Character") && + UiSharedService.CtrlPressed()) + _serverConfigurationManager.RemoveCharacterFromServer(idx, item); + UiSharedService.AttachToolTip("Hold CTRL to delete this entry."); + + i++; + if (item != selectedServer.Authentications.ToList()[^1]) + { + ImGuiHelpers.ScaledDummy(5); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(5); + } + } + + if (selectedServer.Authentications.Any()) + ImGui.Separator(); + + if (!selectedServer.Authentications.Exists(c => + string.Equals(c.CharacterName, youName, StringComparison.Ordinal) + && c.WorldId == youWorld)) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.User, "Add current character")) + { + _serverConfigurationManager.AddCurrentCharacterToServer(idx); + } + + ImGui.SameLine(); + } + + if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new character")) + { + _serverConfigurationManager.AddEmptyCharacterToServer(idx); + } + } + else + { + UiSharedService.ColorTextWrapped("You need to add a Secret Key first before adding Characters.", + UIColors.Get("LightlessYellow")); + } + } + + private void DrawServerSecretKeyManagement(ServerStorage selectedServer) + { + foreach (var item in selectedServer.SecretKeys.ToList()) + { + using var id = ImRaii.PushId("key" + item.Key); + var friendlyName = item.Value.FriendlyName; + if (ImGui.InputText("Secret Key Display Name", ref friendlyName, 255)) + { + item.Value.FriendlyName = friendlyName; + _serverConfigurationManager.Save(); + } + + var key = item.Value.Key; + if (ImGui.InputText("Secret Key", ref key, 64)) + { + item.Value.Key = key; + _serverConfigurationManager.Save(); + } + + if (!selectedServer.Authentications.Exists(p => p.SecretKeyIdx == item.Key)) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && + UiSharedService.CtrlPressed()) + { + selectedServer.SecretKeys.Remove(item.Key); + _serverConfigurationManager.Save(); + } + + UiSharedService.AttachToolTip("Hold CTRL to delete this secret key entry"); + } + else + { + UiSharedService.ColorTextWrapped("This key is in use and cannot be deleted", + UIColors.Get("LightlessYellow")); + } + + if (item.Key != selectedServer.SecretKeys.Keys.LastOrDefault()) + ImGui.Separator(); + } + + ImGui.Separator(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add new Secret Key")) + { + selectedServer.SecretKeys.Add( + selectedServer.SecretKeys.Any() ? selectedServer.SecretKeys.Max(p => p.Key) + 1 : 0, + new SecretKey() { FriendlyName = "New Secret Key", }); + _serverConfigurationManager.Save(); + } + } + + private void DrawServerServiceConfiguration(ServerStorage selectedServer, ref bool useOauth) + { + var serverName = selectedServer.ServerName; + var serverUri = selectedServer.ServerUri; + var isMain = string.Equals(serverName, ApiController.MainServer, StringComparison.OrdinalIgnoreCase); + var flags = isMain ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None; + + if (ImGui.InputText("Service URI", ref serverUri, 255, flags)) + { + selectedServer.ServerUri = serverUri; + } + + if (isMain) + { + _uiShared.DrawHelpText("You cannot edit the URI of the main service."); + } + + if (ImGui.InputText("Service Name", ref serverName, 255, flags)) + { + selectedServer.ServerName = serverName; + _serverConfigurationManager.Save(); + } + + if (isMain) + { + _uiShared.DrawHelpText("You cannot edit the name of the main service."); + } + + ImGui.SetNextItemWidth(200); + var serverTransport = _serverConfigurationManager.GetTransport(); + _uiShared.DrawCombo("Server Transport Type", + Enum.GetValues().Where(t => t != HttpTransportType.None), + (v) => v.ToString(), + onSelected: (t) => _serverConfigurationManager.SetTransportType(t), + serverTransport); + _uiShared.DrawHelpText( + "You normally do not need to change this, if you don't know what this is or what it's for, keep it to WebSockets." + + Environment.NewLine + + "If you run into connection issues with e.g. VPNs, try ServerSentEvents first before trying out LongPolling." + + UiSharedService.TooltipSeparator + + "Note: if the server does not support a specific Transport Type it will fall through to the next automatically: WebSockets > ServerSentEvents > LongPolling"); + + ImGuiHelpers.ScaledDummy(5); + + if (ImGui.Checkbox("Use Discord OAuth2 Authentication", ref useOauth)) + { + selectedServer.UseOAuth2 = useOauth; + _serverConfigurationManager.Save(); + } + + _uiShared.DrawHelpText( + "Use Discord OAuth2 Authentication to identify with this server instead of secret keys"); + if (useOauth) + { + _uiShared.DrawOAuth(selectedServer); + if (string.IsNullOrEmpty(_serverConfigurationManager.GetDiscordUserFromToken(selectedServer))) + { + ImGuiHelpers.ScaledDummy(10f); + UiSharedService.ColorTextWrapped( + "You have enabled OAuth2 but it is not linked. Press the buttons Check, then Authenticate to link properly.", + ImGuiColors.DalamudRed); + } + + if (!string.IsNullOrEmpty(_serverConfigurationManager.GetDiscordUserFromToken(selectedServer)) + && selectedServer.Authentications.TrueForAll(u => string.IsNullOrEmpty(u.UID))) + { + ImGuiHelpers.ScaledDummy(10f); + UiSharedService.ColorTextWrapped( + "You have enabled OAuth2 but no characters configured. Set the correct UIDs for your characters in \"Character Management\".", + ImGuiColors.DalamudRed); + } + } + + if (!isMain && selectedServer != _serverConfigurationManager.CurrentServer) + { + ImGui.Separator(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Service") && + UiSharedService.CtrlPressed()) + { + _serverConfigurationManager.DeleteServer(selectedServer); + } + + _uiShared.DrawHelpText("Hold CTRL to delete this service"); + } + } + + private void DrawServerPermissionSettings(ServerStorage selectedServer) + { + _uiShared.BigText("Default Permission Settings"); + if (selectedServer == _serverConfigurationManager.CurrentServer && _apiController.IsConnected) + { + UiSharedService.TextWrapped( + "Note: The default permissions settings here are not applied retroactively to existing pairs or joined Syncshells."); + UiSharedService.TextWrapped( + "Note: The default permissions settings here are sent and stored on the connected service."); + ImGuiHelpers.ScaledDummy(5f); + var perms = _apiController.DefaultPermissions!; + bool individualIsSticky = perms.IndividualIsSticky; + bool disableIndividualSounds = perms.DisableIndividualSounds; + bool disableIndividualAnimations = perms.DisableIndividualAnimations; + bool disableIndividualVFX = perms.DisableIndividualVFX; + if (ImGui.Checkbox("Individually set permissions become preferred permissions", + ref individualIsSticky)) + { + perms.IndividualIsSticky = individualIsSticky; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText( + "The preferred attribute means that the permissions to that user will never change through any of your permission changes to Syncshells " + + "(i.e. if you have paused one specific user in a Syncshell and they become preferred permissions, then pause and unpause the same Syncshell, the user will remain paused - " + + "if a user does not have preferred permissions, it will follow the permissions of the Syncshell and be unpaused)." + + Environment.NewLine + Environment.NewLine + + "This setting means:" + Environment.NewLine + + " - All new individual pairs get their permissions defaulted to preferred permissions." + + Environment.NewLine + + " - All individually set permissions for any pair will also automatically become preferred permissions. This includes pairs in Syncshells." + + Environment.NewLine + Environment.NewLine + + "It is possible to remove or set the preferred permission state for any pair at any time." + + Environment.NewLine + Environment.NewLine + + "If unsure, leave this setting off."); + ImGuiHelpers.ScaledDummy(3f); + + if (ImGui.Checkbox("Disable individual pair sounds", ref disableIndividualSounds)) + { + perms.DisableIndividualSounds = disableIndividualSounds; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText("This setting will disable sound sync for all new individual pairs."); + if (ImGui.Checkbox("Disable individual pair animations", ref disableIndividualAnimations)) + { + perms.DisableIndividualAnimations = disableIndividualAnimations; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText("This setting will disable animation sync for all new individual pairs."); + if (ImGui.Checkbox("Disable individual pair VFX", ref disableIndividualVFX)) + { + perms.DisableIndividualVFX = disableIndividualVFX; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText("This setting will disable VFX sync for all new individual pairs."); + ImGuiHelpers.ScaledDummy(5f); + bool disableGroundSounds = perms.DisableGroupSounds; + bool disableGroupAnimations = perms.DisableGroupAnimations; + bool disableGroupVFX = perms.DisableGroupVFX; + if (ImGui.Checkbox("Disable Syncshell pair sounds", ref disableGroundSounds)) + { + perms.DisableGroupSounds = disableGroundSounds; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText( + "This setting will disable sound sync for all non-sticky pairs in newly joined syncshells."); + if (ImGui.Checkbox("Disable Syncshell pair animations", ref disableGroupAnimations)) + { + perms.DisableGroupAnimations = disableGroupAnimations; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText( + "This setting will disable animation sync for all non-sticky pairs in newly joined syncshells."); + if (ImGui.Checkbox("Disable Syncshell pair VFX", ref disableGroupVFX)) + { + perms.DisableGroupVFX = disableGroupVFX; + _ = _apiController.UserUpdateDefaultPermissions(perms); + } + + _uiShared.DrawHelpText( + "This setting will disable VFX sync for all non-sticky pairs in newly joined syncshells."); + } + else + { + UiSharedService.ColorTextWrapped("Default Permission Settings unavailable for this service. " + + "You need to connect to this service to change the default permissions since they are stored on the service.", + UIColors.Get("LightlessYellow")); + } + } + private int _lastSelectedServerIndex = -1; private Task<(bool Success, bool PartialSuccess, string Result)>? _secretKeysConversionTask = null; - private CancellationTokenSource _secretKeysConversionCts = new CancellationTokenSource(); + private CancellationTokenSource _secretKeysConversionCts = new(); private async Task<(bool Success, bool partialSuccess, string Result)> ConvertSecretKeysToUIDs( ServerStorage serverStorage, CancellationToken token) { - List failedConversions = serverStorage.Authentications - .Where(u => u.SecretKeyIdx == -1 && string.IsNullOrEmpty(u.UID)).ToList(); - List conversionsToAttempt = serverStorage.Authentications - .Where(u => u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID)).ToList(); + List failedConversions = [.. serverStorage.Authentications.Where(u => u.SecretKeyIdx == -1 && string.IsNullOrEmpty(u.UID))]; + List conversionsToAttempt = [.. serverStorage.Authentications.Where(u => u.SecretKeyIdx != -1 && string.IsNullOrEmpty(u.UID))]; List successfulConversions = []; Dictionary> secretKeyMapping = new(StringComparer.Ordinal); foreach (var authEntry in conversionsToAttempt) @@ -3352,43 +4175,10 @@ public class SettingsUi : WindowMediatorSubscriberBase sb.Append(string.Join(", ", failedConversions.Select(k => k.CharacterName))); } + _secretKeysConversionCts.Dispose(); return (true, failedConversions.Count != 0, sb.ToString()); } - private static string GetLightfinderPresetGlyph(int index) - { - return NameplateHandler.NormalizeIconGlyph( - SeIconCharExtensions.ToIconString(LightfinderIconPresets[index].Icon)); - } - - private void RefreshLightfinderIconState() - { - var normalized = NameplateHandler.NormalizeIconGlyph(_configService.Current.LightfinderLabelIconGlyph); - _lightfinderIconInput = NameplateHandler.ToIconEditorString(normalized); - _lightfinderIconInputInitialized = true; - - _lightfinderIconPresetIndex = -1; - for (int i = 0; i < LightfinderIconPresets.Length; i++) - { - if (string.Equals(GetLightfinderPresetGlyph(i), normalized, StringComparison.Ordinal)) - { - _lightfinderIconPresetIndex = i; - break; - } - } - } - - private void ApplyLightfinderIcon(string normalizedGlyph, int presetIndex) - { - _configService.Current.LightfinderLabelIconGlyph = normalizedGlyph; - _configService.Save(); - _nameplateHandler.FlagRefresh(); - _nameplateService.RequestRedraw(); - _lightfinderIconInput = NameplateHandler.ToIconEditorString(normalizedGlyph); - _lightfinderIconPresetIndex = presetIndex; - _lightfinderIconInputInitialized = true; - } - private void DrawSettingsContent() { if (_apiController.ServerState is ServerState.Connected) @@ -3416,58 +4206,39 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.Separator(); - if (ImGui.BeginTabBar("mainTabBar")) + + if (_selectGeneralTabOnNextDraw) { - var generalTabFlags = ImGuiTabItemFlags.None; - if (_selectGeneralTabOnNextDraw) - { - generalTabFlags |= ImGuiTabItemFlags.SetSelected; - } + _selectedMainTab = MainSettingsTab.General; + _selectGeneralTabOnNextDraw = false; + } - if (ImGui.BeginTabItem("General", generalTabFlags)) - { - _selectGeneralTabOnNextDraw = false; + UiSharedService.Tab("MainSettingsTabs", MainTabOptions, ref _selectedMainTab); + ImGuiHelpers.ScaledDummy(5); + + switch (_selectedMainTab) + { + case MainSettingsTab.General: DrawGeneral(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Performance")) - { + break; + case MainSettingsTab.Performance: DrawPerformance(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Storage")) - { + break; + case MainSettingsTab.Storage: DrawFileStorageSettings(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Transfers")) - { + break; + case MainSettingsTab.Transfers: DrawCurrentTransfers(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Service Settings")) - { + break; + case MainSettingsTab.ServiceSettings: DrawServerConfiguration(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Notifications")) - { + break; + case MainSettingsTab.Notifications: DrawNotificationSettings(); - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Debug")) - { + break; + case MainSettingsTab.Debug: DrawDebug(); - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); + break; } } @@ -3511,7 +4282,7 @@ public class SettingsUi : WindowMediatorSubscriberBase // Lightless notification locations var lightlessLocations = GetLightlessNotificationLocations(); var downloadLocations = GetDownloadNotificationLocations(); - + if (ImGui.BeginTable("##NotificationLocationTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) { ImGui.TableSetupColumn("Notification Type", ImGuiTableColumnFlags.WidthFixed, 200f * ImGuiHelpers.GlobalScale); @@ -3674,7 +4445,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndTable(); } - + ImGuiHelpers.ScaledDummy(5); if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Clear All Notifications")) { @@ -3720,7 +4491,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Unindent(); } - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -3762,7 +4533,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText("Click anywhere on a notification to dismiss it. Notifications with action buttons (like pair requests) are excluded."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -3792,7 +4563,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Spacing(); ImGui.TextUnformatted("Size & Layout"); - + float notifWidth = _configService.Current.NotificationWidth; if (ImGui.SliderFloat("Notification Width", ref notifWidth, 250f, 600f, "%.0f")) { @@ -3825,7 +4596,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Spacing(); ImGui.TextUnformatted("Position"); - + var currentCorner = _configService.Current.NotificationCorner; if (ImGui.BeginCombo("Notification Position", GetNotificationCornerLabel(currentCorner))) { @@ -3843,7 +4614,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndCombo(); } _uiShared.DrawHelpText("Choose which corner of the screen notifications appear in."); - + int offsetY = _configService.Current.NotificationOffsetY; if (ImGui.SliderInt("Vertical Offset", ref offsetY, -2500, 2500)) { @@ -3925,7 +4696,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.SetTooltip("Right click to reset to default (3)."); _uiShared.DrawHelpText("Width of the colored accent bar on the left side."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } } @@ -4020,7 +4791,7 @@ public class SettingsUi : WindowMediatorSubscriberBase if (ImGui.IsItemHovered()) ImGui.SetTooltip("Right click to reset to default (20)."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -4035,7 +4806,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText( "Configure which sounds play for each notification type. Use the play button to preview sounds."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -4083,7 +4854,7 @@ public class SettingsUi : WindowMediatorSubscriberBase "Only show online notifications for pairs where you have set an individual note."); ImGui.Unindent(); - _uiShared.ColoredSeparator(UIColors.Get("LightlessGreen"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessGreen"), 1.5f); ImGui.TreePop(); } @@ -4099,7 +4870,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText( "When you receive a pair request, show Accept/Decline buttons in the notification."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); } @@ -4115,7 +4886,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText( "When a player exceeds performance thresholds or is auto-paused, show Pause/Unpause buttons in the notification."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessOrange"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessOrange"), 1.5f); ImGui.TreePop(); } @@ -4130,42 +4901,42 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared.DrawHelpText("Disable warning notifications for missing optional plugins."); - _uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); ImGui.TreePop(); } ImGui.Separator(); // Location descriptions removed - information is now inline with each setting - + } } - private NotificationLocation[] GetLightlessNotificationLocations() + private static NotificationLocation[] GetLightlessNotificationLocations() { - return new[] - { + return + [ NotificationLocation.LightlessUi, NotificationLocation.Chat, NotificationLocation.ChatAndLightlessUi, NotificationLocation.Nowhere - }; + ]; } - private NotificationLocation[] GetDownloadNotificationLocations() + private static NotificationLocation[] GetDownloadNotificationLocations() { - return new[] - { + return + [ NotificationLocation.LightlessUi, NotificationLocation.TextOverlay, NotificationLocation.Nowhere - }; + ]; } - private NotificationLocation[] GetClassicNotificationLocations() + private static NotificationLocation[] GetClassicNotificationLocations() { - return new[] - { + return + [ NotificationLocation.Toast, NotificationLocation.Chat, NotificationLocation.Both, NotificationLocation.Nowhere - }; + ]; } - private string GetNotificationLocationLabel(NotificationLocation location) + private static string GetNotificationLocationLabel(NotificationLocation location) { return location switch { @@ -4180,7 +4951,7 @@ public class SettingsUi : WindowMediatorSubscriberBase }; } - private string GetNotificationCornerLabel(NotificationCorner corner) + private static string GetNotificationCornerLabel(NotificationCorner corner) { return corner switch { @@ -4256,7 +5027,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.TableSetColumnIndex(2); var availableWidth = ImGui.GetContentRegionAvail().X; var buttonWidth = (availableWidth - ImGui.GetStyle().ItemSpacing.X * 2) / 3; - + // Play button using var playId = ImRaii.PushId($"Play_{typeIndex}"); using (ImRaii.Disabled(isDisabled)) @@ -4277,7 +5048,7 @@ public class SettingsUi : WindowMediatorSubscriberBase } } UiSharedService.AttachToolTip("Test this sound"); - + // Disable toggle button ImGui.SameLine(); using var disableId = ImRaii.PushId($"Disable_{typeIndex}"); @@ -4285,11 +5056,11 @@ public class SettingsUi : WindowMediatorSubscriberBase { var icon = isDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; var color = isDisabled ? UIColors.Get("DimRed") : UIColors.Get("LightlessGreen"); - + ImGui.PushStyleColor(ImGuiCol.Button, color); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, color * new Vector4(1.2f, 1.2f, 1.2f, 1f)); ImGui.PushStyleColor(ImGuiCol.ButtonActive, color * new Vector4(0.8f, 0.8f, 0.8f, 1f)); - + if (ImGui.Button(icon.ToIconString(), new Vector2(buttonWidth, 0))) { bool newDisabled = !isDisabled; @@ -4303,16 +5074,16 @@ public class SettingsUi : WindowMediatorSubscriberBase } _configService.Save(); } - + ImGui.PopStyleColor(3); } UiSharedService.AttachToolTip(isDisabled ? "Sound is disabled - click to enable" : "Sound is enabled - click to disable"); - + // Reset button ImGui.SameLine(); using var resetId = ImRaii.PushId($"Reset_{typeIndex}"); bool isDefault = currentSoundId == defaultSoundId; - + using (ImRaii.Disabled(isDefault)) { using (ImRaii.PushFont(UiBuilder.IconFont)) @@ -4338,5 +5109,3 @@ public class SettingsUi : WindowMediatorSubscriberBase } } } - - diff --git a/LightlessSync/UI/StandaloneProfileUi.cs b/LightlessSync/UI/StandaloneProfileUi.cs index 6ef21d5..d1ebdbe 100644 --- a/LightlessSync/UI/StandaloneProfileUi.cs +++ b/LightlessSync/UI/StandaloneProfileUi.cs @@ -1,12 +1,16 @@ -using Dalamud.Bindings.ImGui; -using Dalamud.Interface.Colors; +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; +using LightlessSync.API.Data; using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto.Group; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; +using LightlessSync.UI.Services; +using LightlessSync.UI.Tags; +using LightlessSync.Utils; using Microsoft.Extensions.Logging; using System.Numerics; @@ -15,167 +19,1249 @@ namespace LightlessSync.UI; public class StandaloneProfileUi : WindowMediatorSubscriberBase { private readonly LightlessProfileManager _lightlessProfileManager; - private readonly PairManager _pairManager; + private readonly PairUiService _pairUiService; private readonly ServerConfigurationManager _serverManager; + private readonly ProfileTagService _profileTagService; private readonly UiSharedService _uiSharedService; - private bool _adjustedForScrollBars = false; + private readonly UserData? _userData; + private readonly GroupData? _groupData; + private readonly bool _isGroupProfile; + private readonly bool _isLightfinderContext; + private readonly string? _lightfinderCid; private byte[] _lastProfilePicture = []; private byte[] _lastSupporterPicture = []; + private byte[] _lastBannerPicture = []; private IDalamudTextureWrap? _supporterTextureWrap; private IDalamudTextureWrap? _textureWrap; + private IDalamudTextureWrap? _bannerTextureWrap; + private bool _bannerTextureLoaded; + private Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f); + private Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f); + private readonly List _seResolvedSegments = new(); + private const float MaxHeightMultiplier = 2.5f; + private const float DescriptionMaxVisibleLines = 12f; + private const string UserDescriptionPlaceholder = "-- User has no description set --"; + private const string GroupDescriptionPlaceholder = "-- Syncshell has no description set --"; + private const string LightfinderDisplayName = "Lightfinder User"; + private readonly string _lightfinderDisplayName = LightfinderDisplayName; + private float _lastComputedWindowHeight = -1f; - public StandaloneProfileUi(ILogger logger, LightlessMediator mediator, UiSharedService uiBuilder, - ServerConfigurationManager serverManager, LightlessProfileManager lightlessProfileManager, PairManager pairManager, Pair pair, + public StandaloneProfileUi( + ILogger logger, + LightlessMediator mediator, + UiSharedService uiBuilder, + ServerConfigurationManager serverManager, + ProfileTagService profileTagService, + DalamudUtilService dalamudUtilService, + LightlessProfileManager lightlessProfileManager, + PairUiService pairUiService, + Pair? pair, + UserData? userData, + GroupData? groupData, + bool isLightfinderContext, + string? lightfinderCid, PerformanceCollectorService performanceCollector) - : base(logger, mediator, "Lightless Profile of " + pair.UserData.AliasOrUID + "##LightlessSyncStandaloneProfileUI" + pair.UserData.AliasOrUID, performanceCollector) + : base(logger, mediator, BuildWindowTitle( + userData, + groupData, + isLightfinderContext, + isLightfinderContext ? ResolveLightfinderDisplayName(dalamudUtilService, lightfinderCid) : null), + performanceCollector) { _uiSharedService = uiBuilder; _serverManager = serverManager; + _profileTagService = profileTagService; _lightlessProfileManager = lightlessProfileManager; Pair = pair; - _pairManager = pairManager; - Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize; + _pairUiService = pairUiService; + _userData = userData; + _groupData = groupData; + _isGroupProfile = groupData is not null; + _isLightfinderContext = isLightfinderContext; + _lightfinderCid = lightfinderCid; + if (_isLightfinderContext) + _lightfinderDisplayName = ResolveLightfinderDisplayName(dalamudUtilService, lightfinderCid); - var spacing = ImGui.GetStyle().ItemSpacing; + Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize; - Size = new(512 + spacing.X * 3 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 512); + var fixedSize = new Vector2(840f, 525f) * ImGuiHelpers.GlobalScale; + Size = fixedSize; + SizeCondition = ImGuiCond.Always; + WindowBuilder.For(this) + .SetSizeConstraints( + fixedSize, + new Vector2(fixedSize.X, fixedSize.Y * MaxHeightMultiplier)) + .Apply(); IsOpen = true; } - public Pair Pair { get; init; } + public Pair? Pair { get; } + public bool IsGroupProfile => _isGroupProfile; + public GroupData? ProfileGroupData => _groupData; + public bool IsLightfinderContext => _isLightfinderContext; + public string? LightfinderCid => _lightfinderCid; + public UserData ProfileUserData => _userData ?? throw new InvalidOperationException("ProfileUserData is only available for user profiles."); + + public void SetTagColorTheme(Vector4? background, Vector4? border) + { + if (background.HasValue) + _tagBackgroundColor = background.Value; + if (border.HasValue) + _tagBorderColor = border.Value; + } + + private static Vector4 ResolveThemeColor(string colorName, Vector4 fallback) + { + try + { + return UIColors.Get(colorName); + } + catch (ArgumentException) + { + // fallback when the color key is not registered + } + + return fallback; + } + + private static string BuildWindowTitle(UserData? userData, GroupData? groupData, bool isLightfinderContext, string? lightfinderDisplayName) + { + if (groupData is not null) + { + var alias = groupData.AliasOrGID; + return $"Syncshell Profile of {alias}##LightlessSyncStandaloneGroupProfileUI{groupData.GID}"; + } + + if (userData is null) + return "Lightless Profile##LightlessSyncStandaloneProfileUI"; + + var name = isLightfinderContext ? lightfinderDisplayName ?? LightfinderDisplayName : userData.AliasOrUID; + var suffix = isLightfinderContext ? " (Lightfinder)" : string.Empty; + return $"Lightless Profile of {name}{suffix}##LightlessSyncStandaloneProfileUI{name}"; + } + + private static string ResolveLightfinderDisplayName(DalamudUtilService dalamudUtilService, string? hashedCid) + { + if (string.IsNullOrEmpty(hashedCid)) + return LightfinderDisplayName; + + try + { + var (name, address) = dalamudUtilService.FindPlayerByNameHash(hashedCid); + if (string.IsNullOrEmpty(name)) + return LightfinderDisplayName; + + var world = dalamudUtilService.GetWorldNameFromPlayerAddress(address); + return string.IsNullOrEmpty(world) ? name : $"{name} ({world})"; + } + catch + { + return LightfinderDisplayName; + } + } protected override void DrawInternal() { try { - var spacing = ImGui.GetStyle().ItemSpacing; - - var lightlessProfile = _lightlessProfileManager.GetLightlessUserProfile(Pair.UserData); - - if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture)) + if (_isGroupProfile) { - _textureWrap?.Dispose(); - _lastProfilePicture = lightlessProfile.ImageData.Value; - _textureWrap = _uiSharedService.LoadImage(_lastProfilePicture); + DrawGroupProfileWindow(); + return; } - if (_supporterTextureWrap == null || !lightlessProfile.SupporterImageData.Value.SequenceEqual(_lastSupporterPicture)) + if (_userData is null) + return; + + var userData = _userData; + var scale = ImGuiHelpers.GlobalScale; + var viewport = ImGui.GetMainViewport(); + var linked = !_isLightfinderContext + && Pair is null + && ProfileEditorLayoutCoordinator.IsActive(userData.UID); + var baseSize = ProfileEditorLayoutCoordinator.GetProfileSize(scale); + var baseWidth = baseSize.X; + var minHeight = baseSize.Y; + var maxAllowedHeight = minHeight * MaxHeightMultiplier; + var targetHeight = _lastComputedWindowHeight > 0f + ? Math.Clamp(_lastComputedWindowHeight, minHeight, maxAllowedHeight) + : minHeight; + var desiredSize = new Vector2(baseWidth, targetHeight); + Size = desiredSize; + if (linked) + { + ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale); + + var currentPos = ImGui.GetWindowPos(); + if (IsWindowBeingDragged()) + ProfileEditorLayoutCoordinator.UpdateAnchorFromProfile(currentPos); + + var desiredPos = ProfileEditorLayoutCoordinator.GetProfilePosition(scale); + if (!ProfileEditorLayoutCoordinator.NearlyEquals(currentPos, desiredPos)) + ImGui.SetWindowPos(desiredPos, ImGuiCond.Always); + + if (!ProfileEditorLayoutCoordinator.NearlyEquals(ImGui.GetWindowSize(), desiredSize)) + ImGui.SetWindowSize(desiredSize, ImGuiCond.Always); + } + else + { + var defaultPosition = viewport.WorkPos + (new Vector2(50f, 70f) * scale); + ImGui.SetWindowPos(defaultPosition, ImGuiCond.FirstUseEver); + ImGui.SetWindowSize(desiredSize, ImGuiCond.Always); + } + + var profile = _lightlessProfileManager.GetLightlessProfile(userData); + IReadOnlyList profileTags = profile.Tags.Count > 0 + ? ProfileTagService.ResolveTags(profile.Tags) + : []; + + if (_textureWrap == null || !profile.ImageData.Value.SequenceEqual(_lastProfilePicture)) + { + _textureWrap?.Dispose(); + _textureWrap = null; + _lastProfilePicture = profile.ImageData.Value; + ResetBannerTexture(); + if (_lastProfilePicture.Length > 0) + { + _textureWrap = _uiSharedService.LoadImage(_lastProfilePicture); + } + } + + if (_supporterTextureWrap == null || !profile.SupporterImageData.Value.SequenceEqual(_lastSupporterPicture)) { _supporterTextureWrap?.Dispose(); _supporterTextureWrap = null; - if (!string.IsNullOrEmpty(lightlessProfile.Base64SupporterPicture)) + if (!string.IsNullOrEmpty(profile.Base64SupporterPicture)) { - _lastSupporterPicture = lightlessProfile.SupporterImageData.Value; - _supporterTextureWrap = _uiSharedService.LoadImage(_lastSupporterPicture); + _lastSupporterPicture = profile.SupporterImageData.Value; + if (_lastSupporterPicture.Length > 0) + { + _supporterTextureWrap = _uiSharedService.LoadImage(_lastSupporterPicture); + } } } + var bannerBytes = profile.BannerImageData.Value; + if (!_lastBannerPicture.SequenceEqual(bannerBytes)) + { + ResetBannerTexture(); + _lastBannerPicture = bannerBytes; + } + + string? noteText = null; + string statusLabel = _isLightfinderContext ? "Exploring" : "Offline"; + string? visiblePlayerName = null; + bool directPair = false; + bool youPaused = false; + bool theyPaused = false; + List syncshellLines = []; + + if (!_isLightfinderContext && Pair != null) + { + var snapshot = _pairUiService.GetSnapshot(); + noteText = _serverManager.GetNoteForUid(Pair.UserData.UID); + statusLabel = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline"); + visiblePlayerName = Pair.IsVisible ? Pair.PlayerName : null; + + directPair = Pair.IsDirectlyPaired; + + var pairInfo = Pair.UserPair; + if (pairInfo != null) + { + if (directPair) + { + youPaused = pairInfo.OwnPermissions.IsPaused(); + theyPaused = pairInfo.OtherPermissions.IsPaused(); + } + + if (pairInfo.Groups.Count != 0) + { + foreach (var gid in pairInfo.Groups) + { + var groupLabel = snapshot.GroupsByGid.TryGetValue(gid, out var groupInfo) + ? groupInfo.GroupAliasOrGID + : gid; + var groupNote = _serverManager.GetNoteForGid(gid); + syncshellLines.Add(string.IsNullOrEmpty(groupNote) ? groupLabel : $"{groupNote} ({groupLabel})"); + } + } + } + } + + var presenceTokens = new List + { + new(statusLabel, string.Equals(statusLabel, "Offline", StringComparison.OrdinalIgnoreCase)) + }; + + if (!string.IsNullOrEmpty(visiblePlayerName)) + presenceTokens.Add(new PresenceToken(visiblePlayerName, false)); + + if (directPair) + { + presenceTokens.Add(new PresenceToken("Direct Pair", true)); + if (youPaused) + presenceTokens.Add(new PresenceToken("You paused syncing", true)); + if (theyPaused) + presenceTokens.Add(new PresenceToken("They paused syncing", true)); + } + + if (profile.IsNSFW) + presenceTokens.Add(new PresenceToken("NSFW", Emphasis: true)); + + if (syncshellLines.Count > 0) + presenceTokens.Add(new PresenceToken($"Sharing Syncshells ({syncshellLines.Count})", Emphasis: false, syncshellLines, "Shared Syncshells")); + var drawList = ImGui.GetWindowDrawList(); - var rectMin = drawList.GetClipRectMin(); - var rectMax = drawList.GetClipRectMax(); - var headerSize = ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y; + var style = ImGui.GetStyle(); + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + var bannerHeight = 260f * scale; + var portraitSize = new Vector2(180f, 180f) * scale; + var portraitBorder = 1.5f * scale; + var portraitRounding = 1f * scale; + var portraitFrameSize = portraitSize + new Vector2(portraitBorder * 2f); + var portraitOverlap = portraitSize.Y * 0.35f; + var portraitOffsetX = 35f * scale; + var infoOffsetX = portraitOffsetX + portraitFrameSize.X + style.ItemSpacing.X * 2f; + var bannerTexture = GetBannerTexture(_lastBannerPicture) ?? _textureWrap ?? _supporterTextureWrap; - using (_uiSharedService.UidFont.Push()) - UiSharedService.ColorText(Pair.UserData.AliasOrUID, UIColors.Get("LightlessBlue")); + string defaultSubtitle = !_isLightfinderContext && Pair != null && !string.IsNullOrEmpty(Pair.UserData.Alias) + ? Pair.UserData.Alias! + : _isLightfinderContext ? "Lightfinder Session" : noteText ?? string.Empty; - ImGuiHelpers.ScaledDummy(new Vector2(spacing.Y, spacing.Y)); - var textPos = ImGui.GetCursorPosY() - headerSize; - ImGui.Separator(); - var pos = ImGui.GetCursorPos() with { Y = ImGui.GetCursorPosY() - headerSize }; - ImGuiHelpers.ScaledDummy(new Vector2(256, 256 + spacing.Y)); - var postDummy = ImGui.GetCursorPosY(); - ImGui.SameLine(); - var descriptionTextSize = ImGui.CalcTextSize(lightlessProfile.Description, wrapWidth: 256f); - var descriptionChildHeight = rectMax.Y - pos.Y - rectMin.Y - spacing.Y * 2; - if (descriptionTextSize.Y > descriptionChildHeight && !_adjustedForScrollBars) - { - Size = Size!.Value with { X = Size.Value.X + ImGui.GetStyle().ScrollbarSize }; - _adjustedForScrollBars = true; - } - else if (descriptionTextSize.Y < descriptionChildHeight && _adjustedForScrollBars) - { - Size = Size!.Value with { X = Size.Value.X - ImGui.GetStyle().ScrollbarSize }; - _adjustedForScrollBars = false; - } - var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, descriptionChildHeight); - childFrame = childFrame with - { - X = childFrame.X + (_adjustedForScrollBars ? ImGui.GetStyle().ScrollbarSize : 0), - Y = childFrame.Y / ImGuiHelpers.GlobalScale - }; - if (ImGui.BeginChildFrame(1000, childFrame)) - { - using var _ = _uiSharedService.GameFont.Push(); - ImGui.TextWrapped(lightlessProfile.Description); - } - ImGui.EndChildFrame(); + bool hasVanityAlias = !_isLightfinderContext && userData.HasVanity && !string.IsNullOrWhiteSpace(userData.Alias); + Vector4? vanityTextColor = null; + Vector4? vanityGlowColor = null; - ImGui.SetCursorPosY(postDummy); - var note = _serverManager.GetNoteForUid(Pair.UserData.UID); - if (!string.IsNullOrEmpty(note)) + if (hasVanityAlias) { - UiSharedService.ColorText(note, ImGuiColors.DalamudGrey); + if (!string.IsNullOrWhiteSpace(userData.TextColorHex)) + vanityTextColor = UIColors.HexToRgba(userData.TextColorHex); + + if (!string.IsNullOrWhiteSpace(userData.TextGlowColorHex)) + vanityGlowColor = UIColors.HexToRgba(userData.TextGlowColorHex); } - string status = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline"); - UiSharedService.ColorText(status, (Pair.IsVisible || Pair.IsOnline) ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudRed); - if (Pair.IsVisible) + + bool useVanityColors = vanityTextColor.HasValue || vanityGlowColor.HasValue; + string primaryHeaderText = _isLightfinderContext + ? _lightfinderDisplayName + : hasVanityAlias ? userData.Alias! : userData.UID; + + List<(string Text, bool UseVanityColor, bool Disabled)> secondaryHeaderLines = new(); + if (!_isLightfinderContext && hasVanityAlias) { - ImGui.SameLine(); - ImGui.TextUnformatted($"({Pair.PlayerName})"); - } - if (Pair.UserPair != null) - { - ImGui.TextUnformatted("Directly paired"); - if (Pair.UserPair.OwnPermissions.IsPaused()) + secondaryHeaderLines.Add((userData.UID, useVanityColors, false)); + + if (!string.IsNullOrEmpty(defaultSubtitle) + && !string.Equals(defaultSubtitle, userData.UID, StringComparison.OrdinalIgnoreCase) + && !string.Equals(defaultSubtitle, userData.Alias, StringComparison.OrdinalIgnoreCase)) { - ImGui.SameLine(); - UiSharedService.ColorText("You: paused", UIColors.Get("LightlessYellow")); - } - if (Pair.UserPair.OtherPermissions.IsPaused()) - { - ImGui.SameLine(); - UiSharedService.ColorText("They: paused", UIColors.Get("LightlessYellow")); + secondaryHeaderLines.Add((defaultSubtitle, false, true)); } } - - if (Pair.UserPair.Groups.Any()) + else if (!string.IsNullOrEmpty(defaultSubtitle) + && !string.Equals(defaultSubtitle, userData.UID, StringComparison.OrdinalIgnoreCase)) { - ImGui.TextUnformatted("Paired through Syncshells:"); - foreach (var group in Pair.UserPair.Groups) - { - var groupNote = _serverManager.GetNoteForGid(group); - var groupName = _pairManager.GroupPairs.First(f => string.Equals(f.Key.GID, group, StringComparison.Ordinal)).Key.GroupAliasOrGID; - var groupString = string.IsNullOrEmpty(groupNote) ? groupName : $"{groupNote} ({groupName})"; - ImGui.TextUnformatted("- " + groupString); - } + secondaryHeaderLines.Add((defaultSubtitle, false, true)); + } + + var bannerScrollOffset = new Vector2(ImGui.GetScrollX(), ImGui.GetScrollY()); + var bannerMin = windowPos - bannerScrollOffset; + var bannerMax = bannerMin + new Vector2(windowSize.X, bannerHeight); + + if (bannerTexture != null) + { + drawList.AddImage( + bannerTexture.Handle, + bannerMin, + bannerMax); + } + else + { + var headerBase = ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.Header)); + var topColor = ResolveThemeColor("ProfileBodyGradientTop", Vector4.Lerp(headerBase, Vector4.One, 0.25f)); + topColor.W = 1f; + var bottomColor = ResolveThemeColor("ProfileBodyGradientBottom", Vector4.Lerp(headerBase, Vector4.Zero, 0.35f)); + bottomColor.W = 1f; + + drawList.AddRectFilledMultiColor( + bannerMin, + bannerMax, + ImGui.ColorConvertFloat4ToU32(topColor), + ImGui.ColorConvertFloat4ToU32(topColor), + ImGui.ColorConvertFloat4ToU32(bottomColor), + ImGui.ColorConvertFloat4ToU32(bottomColor)); } - var padding = ImGui.GetStyle().WindowPadding.X / 2; - bool tallerThanWide = _textureWrap.Height >= _textureWrap.Width; - var stretchFactor = tallerThanWide ? 256f * ImGuiHelpers.GlobalScale / _textureWrap.Height : 256f * ImGuiHelpers.GlobalScale / _textureWrap.Width; - var newWidth = _textureWrap.Width * stretchFactor; - var newHeight = _textureWrap.Height * stretchFactor; - var remainingWidth = (256f * ImGuiHelpers.GlobalScale - newWidth) / 2f; - var remainingHeight = (256f * ImGuiHelpers.GlobalScale - newHeight) / 2f; - drawList.AddImage(_textureWrap.Handle, new Vector2(rectMin.X + padding + remainingWidth, rectMin.Y + spacing.Y + pos.Y + remainingHeight), - new Vector2(rectMin.X + padding + remainingWidth + newWidth, rectMin.Y + spacing.Y + pos.Y + remainingHeight + newHeight)); if (_supporterTextureWrap != null) { - const float iconSize = 38; - drawList.AddImage(_supporterTextureWrap.Handle, - new Vector2(rectMax.X - iconSize - spacing.X, rectMin.Y + (textPos / 2) - (iconSize / 2)), - new Vector2(rectMax.X - spacing.X, rectMin.Y + iconSize + (textPos / 2) - (iconSize / 2))); + const float iconBaseSize = 40f; + var iconPadding = new Vector2(style.WindowPadding.X + 18f * scale, style.WindowPadding.Y + 18f * scale); + var textureWidth = MathF.Max(1f, _supporterTextureWrap.Width); + var textureHeight = MathF.Max(1f, _supporterTextureWrap.Height); + var textureMaxEdge = MathF.Max(textureWidth, textureHeight); + var iconScale = (iconBaseSize * scale) / textureMaxEdge; + var iconSize = new Vector2(textureWidth * iconScale, textureHeight * iconScale); + var iconMax = bannerMax - iconPadding; + var iconMin = iconMax - iconSize; + var backgroundPadding = 6f * scale; + var iconBackgroundMin = iconMin - new Vector2(backgroundPadding); + var iconBackgroundMax = iconMax + new Vector2(backgroundPadding); + var backgroundColor = new Vector4(0f, 0f, 0f, 0.65f); + var cornerRadius = MathF.Max(4f * scale, iconSize.Y * 0.25f); + + drawList.AddRectFilled(iconBackgroundMin, iconBackgroundMax, ImGui.GetColorU32(backgroundColor), cornerRadius); + drawList.AddImage(_supporterTextureWrap.Handle, iconMin, iconMax); } + + var contentStartY = MathF.Max(style.WindowPadding.Y, bannerHeight - portraitOverlap); + var topAreaStart = ImGui.GetCursorPos(); + + var portraitBackgroundPadding = 12f * scale; + var portraitAreaSize = portraitFrameSize + new Vector2(portraitBackgroundPadding * 2f); + var portraitAreaPos = new Vector2(style.WindowPadding.X + portraitOffsetX - portraitBackgroundPadding, contentStartY - portraitBackgroundPadding - 24f * scale); + + ImGui.SetCursorPos(portraitAreaPos); + var portraitAreaScreenPos = ImGui.GetCursorScreenPos(); + ImGui.Dummy(portraitAreaSize); + + var portraitAreaMin = portraitAreaScreenPos; + var portraitAreaMax = portraitAreaMin + portraitAreaSize; + var portraitFrameMin = portraitAreaMin + new Vector2(portraitBackgroundPadding); + var portraitFrameMax = portraitFrameMin + portraitFrameSize; + + var portraitAreaColor = style.Colors[(int)ImGuiCol.WindowBg]; + portraitAreaColor.W = MathF.Min(1f, portraitAreaColor.W + 0.2f); + drawList.AddRectFilled(portraitAreaMin, portraitAreaMax, ImGui.GetColorU32(portraitAreaColor), portraitRounding + portraitBorder + portraitBackgroundPadding); + + var portraitFrameBorder = style.Colors[(int)ImGuiCol.Border]; + + if (_textureWrap != null) + { + drawList.AddImageRounded( + _textureWrap.Handle, + portraitFrameMin + new Vector2(portraitBorder, portraitBorder), + portraitFrameMax - new Vector2(portraitBorder, portraitBorder), + Vector2.Zero, + Vector2.One, + 0xFFFFFFFF, + portraitRounding); + } + else + { + drawList.AddRect( + portraitFrameMin + new Vector2(portraitBorder, portraitBorder), + portraitFrameMax - new Vector2(portraitBorder, portraitBorder), + ImGui.GetColorU32(portraitFrameBorder), + portraitRounding); + } + + var portraitAreaLocalMin = portraitAreaMin - windowPos; + var portraitAreaLocalMax = portraitAreaMax - windowPos; + var portraitFrameLocalMin = portraitFrameMin - windowPos; + var portraitFrameLocalMax = portraitFrameMax - windowPos; + var portraitBlockBottom = windowPos.Y + portraitAreaLocalMax.Y; + + var infoStartY = MathF.Max(contentStartY, bannerHeight + style.WindowPadding.Y); + var aliasColumnX = infoOffsetX + 18f * scale; + ImGui.SetCursorPos(new Vector2(aliasColumnX, infoStartY)); + + ImGui.BeginGroup(); + using (_uiSharedService.UidFont.Push()) + { + if (useVanityColors) + { + var seString = SeStringUtils.BuildFormattedPlayerName(primaryHeaderText, vanityTextColor, vanityGlowColor); + SeStringUtils.RenderSeStringWithHitbox(seString, ImGui.GetCursorScreenPos(), ImGui.GetFont()); + } + else + { + ImGui.TextUnformatted(primaryHeaderText); + } + } + + foreach (var (text, useColor, disabled) in secondaryHeaderLines) + { + if (useColor && useVanityColors) + { + var seString = SeStringUtils.BuildFormattedPlayerName(text, vanityTextColor, vanityGlowColor); + SeStringUtils.RenderSeStringWithHitbox(seString, ImGui.GetCursorScreenPos(), ImGui.GetFont()); + } + else + { + if (disabled) + ImGui.TextDisabled(text); + else + ImGui.TextUnformatted(text); + } + } + ImGui.EndGroup(); + var namesEnd = ImGui.GetCursorPos(); + + var namesBlockBottom = windowPos.Y + namesEnd.Y; + var aliasGroupRectMin = ImGui.GetItemRectMin(); + var aliasGroupRectMax = ImGui.GetItemRectMax(); + var aliasGroupLocalMin = aliasGroupRectMin - windowPos; + var aliasGroupLocalMax = aliasGroupRectMax - windowPos; + + var tagsStartLocal = new Vector2(aliasGroupLocalMax.X + style.ItemSpacing.X + 25f * scale, aliasGroupLocalMin.Y + style.FramePadding.Y + 2f * scale); + ImGui.SetCursorPos(tagsStartLocal); + RenderProfileTags(profileTags, scale); + var tagsEndLocal = ImGui.GetCursorPos(); + var tagsBlockBottom = windowPos.Y + tagsEndLocal.Y; + var aliasBlockBottom = windowPos.Y + aliasGroupLocalMax.Y; + var aliasAndTagsBottomLocal = MathF.Max(aliasGroupLocalMax.Y, tagsEndLocal.Y); + var aliasAndTagsBlockBottom = MathF.Max(aliasBlockBottom, tagsBlockBottom); + + var descriptionPreSpacing = style.ItemSpacing.Y * 1.35f; + var descriptionStartLocal = new Vector2(aliasColumnX, aliasAndTagsBottomLocal + descriptionPreSpacing); + var horizontalInset = style.ItemSpacing.X * 0.5f; + var descriptionSeparatorSpacing = style.ItemSpacing.Y * 0.5f; + var descriptionSeparatorThickness = MathF.Max(1f, scale); + var descriptionSeparatorStart = windowPos + new Vector2(aliasColumnX - horizontalInset, descriptionStartLocal.Y); + var descriptionSeparatorEnd = new Vector2(windowPos.X + windowSize.X - style.WindowPadding.X + horizontalInset, descriptionSeparatorStart.Y); + drawList.AddLine(descriptionSeparatorStart, descriptionSeparatorEnd, ImGui.GetColorU32(portraitFrameBorder), descriptionSeparatorThickness); + + var descriptionContentStartLocal = descriptionStartLocal + new Vector2(0f, descriptionSeparatorThickness + descriptionSeparatorSpacing + style.FramePadding.Y * 0.75f); + ImGui.SetCursorPos(descriptionContentStartLocal); + ImGui.TextDisabled("Description"); + ImGui.SetCursorPosX(aliasColumnX); + var descriptionRegionWidth = ImGui.GetContentRegionAvail().X; + if (descriptionRegionWidth <= 0f) + descriptionRegionWidth = 1f; + var measurementWrapWidth = MathF.Max(1f, descriptionRegionWidth - style.WindowPadding.X * 2f); + var hasDescription = !string.IsNullOrWhiteSpace(profile.Description); + float descriptionContentHeight; + float lineHeightWithSpacing; + using (_uiSharedService.GameFont.Push()) + { + lineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing(); + var measurementText = hasDescription + ? NormalizeDescriptionForMeasurement(profile.Description!) + : UserDescriptionPlaceholder; + if (string.IsNullOrWhiteSpace(measurementText)) + measurementText = UserDescriptionPlaceholder; + + descriptionContentHeight = ImGui.CalcTextSize(measurementText, wrapWidth: measurementWrapWidth).Y; + if (descriptionContentHeight <= 0f) + descriptionContentHeight = lineHeightWithSpacing; + } + + var maxDescriptionHeight = lineHeightWithSpacing * DescriptionMaxVisibleLines; + var descriptionChildHeight = Math.Clamp(descriptionContentHeight, lineHeightWithSpacing, maxDescriptionHeight); + + RenderDescriptionChild( + "##StandaloneProfileDescription", + new Vector2(descriptionRegionWidth, descriptionChildHeight), + hasDescription ? profile.Description : null, + UserDescriptionPlaceholder); + + var descriptionEndLocal = ImGui.GetCursorPos(); + var descriptionBlockBottom = windowPos.Y + descriptionEndLocal.Y; + aliasAndTagsBottomLocal = MathF.Max(aliasAndTagsBottomLocal, descriptionEndLocal.Y); + aliasAndTagsBlockBottom = MathF.Max(aliasAndTagsBlockBottom, descriptionBlockBottom); + + var presenceLabelSpacing = style.ItemSpacing.Y * 0.35f; + var presenceAnchorY = MathF.Max(portraitFrameLocalMax.Y, aliasGroupLocalMax.Y); + var presenceStartLocal = new Vector2( + portraitFrameLocalMin.X, + presenceAnchorY + presenceLabelSpacing); + ImGui.SetCursorPos(presenceStartLocal); + ImGui.TextDisabled("Presence"); + ImGui.SetCursorPosX(portraitFrameLocalMin.X); + if (presenceTokens.Count > 0) + { + var presenceColumnWidth = MathF.Max(1f, aliasColumnX - portraitFrameLocalMin.X - style.ItemSpacing.X); + RenderPresenceTokens(presenceTokens, scale, presenceColumnWidth); + } + else + { + ImGui.SetCursorPosX(portraitFrameLocalMin.X); + ImGui.TextDisabled("-- No presence information --"); + ImGui.SetCursorPosX(portraitFrameLocalMin.X); + ImGui.Dummy(new Vector2(0f, style.ItemSpacing.Y * 0.25f)); + } + + var presenceContentEnd = ImGui.GetCursorPos(); + var separatorSpacing = style.ItemSpacing.Y * 0.2f; + var separatorThickness = MathF.Max(1f, scale); + var separatorStartLocal = new Vector2(portraitFrameLocalMin.X, presenceContentEnd.Y + separatorSpacing); + var separatorStart = windowPos + separatorStartLocal; + var separatorEnd = new Vector2(portraitFrameMax.X, separatorStart.Y); + drawList.AddLine(separatorStart, separatorEnd, ImGui.GetColorU32(portraitFrameBorder), separatorThickness); + var afterSeparatorLocal = separatorStartLocal + new Vector2(0f, separatorThickness + separatorSpacing * 0.75f); + + var columnStartLocalY = afterSeparatorLocal.Y; + var leftColumnX = portraitFrameLocalMin.X; + var leftWrapPos = windowPos.X + aliasColumnX - style.ItemSpacing.X; + + ImGui.SetCursorPos(new Vector2(leftColumnX, columnStartLocalY)); + float leftColumnEndY = columnStartLocalY; + + if (!string.IsNullOrEmpty(noteText)) + { + ImGui.TextDisabled("Notes"); + ImGui.SetCursorPosX(leftColumnX); + ImGui.PushTextWrapPos(leftWrapPos); + ImGui.TextUnformatted(noteText); + ImGui.PopTextWrapPos(); + ImGui.SetCursorPos(new Vector2(leftColumnX, ImGui.GetCursorPosY() + style.ItemSpacing.Y * 0.5f)); + leftColumnEndY = ImGui.GetCursorPosY(); + } + + leftColumnEndY = MathF.Max(leftColumnEndY, ImGui.GetCursorPosY()); + + var columnsBottomLocal = leftColumnEndY; + var columnsBottom = windowPos.Y + columnsBottomLocal; + var topAreaBase = windowPos.Y + topAreaStart.Y; + var contentBlockBottom = MathF.Max(columnsBottom, aliasAndTagsBlockBottom); + var leftBlockBottom = MathF.Max(portraitBlockBottom, contentBlockBottom); + var topAreaHeight = leftBlockBottom - topAreaBase; + if (topAreaHeight < 0f) + topAreaHeight = 0f; + + ImGui.SetCursorPos(new Vector2(leftColumnX, topAreaStart.Y + topAreaHeight + style.ItemSpacing.Y)); + + var finalCursorY = ImGui.GetCursorPosY(); + var paddingY = ImGui.GetStyle().WindowPadding.Y; + var computedHeight = finalCursorY + paddingY; + var adjustedHeight = Math.Clamp(computedHeight, minHeight, maxAllowedHeight); + _lastComputedWindowHeight = adjustedHeight; + + var finalSize = new Vector2(baseWidth, adjustedHeight); + Size = finalSize; + ImGui.SetWindowSize(finalSize, ImGuiCond.Always); } catch (Exception ex) { - _logger.LogWarning(ex, "Error during draw tooltip"); + _logger.LogWarning(ex, "Error during standalone profile draw"); } } + private IDalamudTextureWrap? GetIconWrap(uint iconId) + { + try + { + if (_uiSharedService.TryGetIcon(iconId, out var wrap) && wrap != null) + return wrap; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to resolve icon {IconId} for profile tags", iconId); + } + + return null; + } + + private void RenderDescriptionChild( + string childId, + Vector2 childSize, + string? description, + string placeholderText) + { + ImGui.PushStyleVar(ImGuiStyleVar.ChildBorderSize, 0f); + if (ImGui.BeginChild(childId, childSize, false)) + { + using (_uiSharedService.GameFont.Push()) + { + ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X); + if (string.IsNullOrWhiteSpace(description)) + { + ImGui.TextUnformatted(placeholderText); + } + else if (!SeStringUtils.TryRenderSeStringMarkupAtCursor(description)) + { + ImGui.TextUnformatted(description); + } + ImGui.PopTextWrapPos(); + } + } + ImGui.EndChild(); + ImGui.PopStyleVar(); + } + + private void RenderProfileTags(IReadOnlyList tags, float scale) + { + if (tags.Count == 0) + { + ImGui.TextDisabled("-- No tags set --"); + return; + } + + var drawList = ImGui.GetWindowDrawList(); + var style = ImGui.GetStyle(); + var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text); + + var startLocal = ImGui.GetCursorPos(); + var startScreen = ImGui.GetCursorScreenPos(); + float availableWidth = ImGui.GetContentRegionAvail().X; + if (availableWidth <= 0f) + availableWidth = 1f; + + float cursorX = startScreen.X; + float cursorY = startScreen.Y; + float rowHeight = 0f; + + for (int i = 0; i < tags.Count; i++) + { + var tag = tags[i]; + if (!tag.HasContent) + continue; + + var tagSize = ProfileTagRenderer.MeasureTag(tag, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger); + var tagWidth = tagSize.X; + var tagHeight = tagSize.Y; + + if (cursorX > startScreen.X && cursorX + tagWidth > startScreen.X + availableWidth) + { + cursorX = startScreen.X; + cursorY += rowHeight + style.ItemSpacing.Y; + rowHeight = 0f; + } + + var tagPos = new Vector2(cursorX, cursorY); + ImGui.SetCursorScreenPos(tagPos); + ImGui.InvisibleButton($"##profileTag_{i}", tagSize); + ProfileTagRenderer.RenderTag(tag, tagPos, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger); + + cursorX += tagWidth + style.ItemSpacing.X; + rowHeight = MathF.Max(rowHeight, tagHeight); + } + + var totalHeight = (cursorY + rowHeight) - startScreen.Y; + if (totalHeight < 0f) + totalHeight = 0f; + + ImGui.SetCursorPos(new Vector2(startLocal.X, startLocal.Y + totalHeight)); + } + + private void DrawGroupProfileWindow() + { + if (_groupData is null) + return; + + var scale = ImGuiHelpers.GlobalScale; + var viewport = ImGui.GetMainViewport(); + var linked = ProfileEditorLayoutCoordinator.IsActive(_groupData.GID); + var baseSize = ProfileEditorLayoutCoordinator.GetProfileSize(scale); + var baseWidth = baseSize.X; + var minHeight = baseSize.Y; + var maxAllowedHeight = minHeight * MaxHeightMultiplier; + var targetHeight = _lastComputedWindowHeight > 0f + ? Math.Clamp(_lastComputedWindowHeight, minHeight, maxAllowedHeight) + : minHeight; + var desiredSize = new Vector2(baseWidth, targetHeight); + Size = desiredSize; + + if (linked) + { + ProfileEditorLayoutCoordinator.EnsureAnchor(viewport.WorkPos, scale); + + var currentPos = ImGui.GetWindowPos(); + if (IsWindowBeingDragged()) + ProfileEditorLayoutCoordinator.UpdateAnchorFromProfile(currentPos); + + var desiredPos = ProfileEditorLayoutCoordinator.GetProfilePosition(scale); + if (!ProfileEditorLayoutCoordinator.NearlyEquals(currentPos, desiredPos)) + ImGui.SetWindowPos(desiredPos, ImGuiCond.Always); + + if (!ProfileEditorLayoutCoordinator.NearlyEquals(ImGui.GetWindowSize(), desiredSize)) + ImGui.SetWindowSize(desiredSize, ImGuiCond.Always); + } + else + { + var defaultPosition = viewport.WorkPos + (new Vector2(50f, 70f) * scale); + ImGui.SetWindowPos(defaultPosition, ImGuiCond.FirstUseEver); + ImGui.SetWindowSize(desiredSize, ImGuiCond.Always); + } + + var profile = _lightlessProfileManager.GetLightlessGroupProfile(_groupData); + IReadOnlyList profileTags = profile.Tags.Count > 0 + ? ProfileTagService.ResolveTags(profile.Tags) + : Array.Empty(); + + if (_textureWrap == null || !profile.ProfileImageData.Value.SequenceEqual(_lastProfilePicture)) + { + _textureWrap?.Dispose(); + _textureWrap = null; + _lastProfilePicture = profile.ProfileImageData.Value; + ResetBannerTexture(); + if (_lastProfilePicture.Length > 0) + { + _textureWrap = _uiSharedService.LoadImage(_lastProfilePicture); + } + } + + if (_supporterTextureWrap != null) + { + _supporterTextureWrap.Dispose(); + _supporterTextureWrap = null; + } + _lastSupporterPicture = Array.Empty(); + + var bannerBytes = profile.BannerImageData.Value; + if (!_lastBannerPicture.SequenceEqual(bannerBytes)) + { + ResetBannerTexture(); + _lastBannerPicture = bannerBytes; + } + + var noteText = _serverManager.GetNoteForGid(_groupData.GID); + + var presenceTokens = new List + { + new(profile.IsDisabled ? "Disabled" : "Active", profile.IsDisabled) + }; + + if (profile.IsNsfw) + presenceTokens.Add(new PresenceToken("NSFW", Emphasis: true)); + + int memberCount = 0; + List? groupMembers = null; + var snapshot = _pairUiService.GetSnapshot(); + GroupFullInfoDto? groupInfo = null; + if (_groupData is not null && snapshot.GroupsByGid.TryGetValue(_groupData.GID, out var refreshedGroupInfo)) + { + groupInfo = refreshedGroupInfo; + } + if (groupInfo is not null && snapshot.GroupPairs.TryGetValue(groupInfo, out var pairsForGroup)) + { + groupMembers = pairsForGroup.ToList(); + memberCount = groupMembers.Count; + } + else if (groupInfo?.GroupPairUserInfos is { Count: > 0 }) + { + memberCount = groupInfo.GroupPairUserInfos.Count; + } + + string memberLabel = memberCount == 1 ? "1 Member" : $"{memberCount} Members"; + presenceTokens.Add(new PresenceToken(memberLabel, false)); + + if (groupInfo?.GroupPermissions.IsDisableInvites() ?? false) + { + presenceTokens.Add(new PresenceToken( + "Invites Locked", + true, + new[] + { + "New members cannot join while this lock is active." + }, + "Syncshell Status")); + } + + var drawList = ImGui.GetWindowDrawList(); + var style = ImGui.GetStyle(); + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + var bannerHeight = 260f * scale; + var portraitSize = new Vector2(180f, 180f) * scale; + var portraitBorder = 1.5f * scale; + var portraitRounding = 1f * scale; + var portraitFrameSize = portraitSize + new Vector2(portraitBorder * 2f); + var portraitOverlap = portraitSize.Y * 0.35f; + var portraitOffsetX = 35f * scale; + var infoOffsetX = portraitOffsetX + portraitFrameSize.X + style.ItemSpacing.X * 2f; + var bannerTexture = GetBannerTexture(_lastBannerPicture) ?? _textureWrap; + + var bannerScrollOffset = new Vector2(ImGui.GetScrollX(), ImGui.GetScrollY()); + var bannerMin = windowPos - bannerScrollOffset; + var bannerMax = bannerMin + new Vector2(windowSize.X, bannerHeight); + + if (bannerTexture != null) + { + drawList.AddImage( + bannerTexture.Handle, + bannerMin, + bannerMax); + } + else + { + var headerBase = ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.Header)); + var topColor = ResolveThemeColor("ProfileBodyGradientTop", Vector4.Lerp(headerBase, Vector4.One, 0.25f)); + topColor.W = 1f; + var bottomColor = ResolveThemeColor("ProfileBodyGradientBottom", Vector4.Lerp(headerBase, Vector4.Zero, 0.35f)); + bottomColor.W = 1f; + + drawList.AddRectFilledMultiColor( + bannerMin, + bannerMax, + ImGui.ColorConvertFloat4ToU32(topColor), + ImGui.ColorConvertFloat4ToU32(topColor), + ImGui.ColorConvertFloat4ToU32(bottomColor), + ImGui.ColorConvertFloat4ToU32(bottomColor)); + } + + var contentStartY = MathF.Max(style.WindowPadding.Y, bannerHeight - portraitOverlap); + var topAreaStart = ImGui.GetCursorPos(); + + var portraitBackgroundPadding = 12f * scale; + var portraitAreaSize = portraitFrameSize + new Vector2(portraitBackgroundPadding * 2f); + var portraitAreaPos = new Vector2(style.WindowPadding.X + portraitOffsetX - portraitBackgroundPadding, contentStartY - portraitBackgroundPadding - 24f * scale); + + ImGui.SetCursorPos(portraitAreaPos); + var portraitAreaScreenPos = ImGui.GetCursorScreenPos(); + ImGui.Dummy(portraitAreaSize); + var contentStart = ImGui.GetCursorPos(); + + var portraitAreaMin = portraitAreaScreenPos; + var portraitAreaMax = portraitAreaMin + portraitAreaSize; + var portraitFrameMin = portraitAreaMin + new Vector2(portraitBackgroundPadding); + var portraitFrameMax = portraitFrameMin + portraitFrameSize; + + var portraitAreaColor = style.Colors[(int)ImGuiCol.WindowBg]; + portraitAreaColor.W = MathF.Min(1f, portraitAreaColor.W + 0.2f); + drawList.AddRectFilled(portraitAreaMin, portraitAreaMax, ImGui.GetColorU32(portraitAreaColor), portraitRounding + portraitBorder + portraitBackgroundPadding); + var portraitFrameBorder = style.Colors[(int)ImGuiCol.Border]; + + if (_textureWrap != null) + { + drawList.AddImageRounded( + _textureWrap.Handle, + portraitFrameMin + new Vector2(portraitBorder, portraitBorder), + portraitFrameMax - new Vector2(portraitBorder, portraitBorder), + Vector2.Zero, + Vector2.One, + 0xFFFFFFFF, + portraitRounding); + } + else + { + drawList.AddRect( + portraitFrameMin + new Vector2(portraitBorder, portraitBorder), + portraitFrameMax - new Vector2(portraitBorder, portraitBorder), + ImGui.GetColorU32(portraitFrameBorder), + portraitRounding); + } + + drawList.AddRect(portraitFrameMin, portraitFrameMax, ImGui.GetColorU32(portraitFrameBorder), portraitRounding); + var portraitAreaLocalMax = portraitAreaMax - windowPos; + var portraitFrameLocalMin = portraitFrameMin - windowPos; + var portraitFrameLocalMax = portraitFrameMax - windowPos; + var portraitBlockBottom = windowPos.Y + portraitAreaLocalMax.Y; + + ImGui.SetCursorPos(contentStart); + + bool useVanityColors = false; + Vector4? vanityTextColor = null; + Vector4? vanityGlowColor = null; + string primaryHeaderText = _groupData.AliasOrGID; + + List<(string Text, bool UseVanityColor, bool Disabled)> secondaryHeaderLines = + [ + (_groupData.GID, false, true) + ]; + + if (groupInfo is not null) + secondaryHeaderLines.Add(($"Owner: {groupInfo.Owner.AliasOrUID}", false, true)); + else + secondaryHeaderLines.Add(($"Unknown Owner", false, true)); + + var infoStartY = MathF.Max(contentStartY, bannerHeight + style.WindowPadding.Y); + var aliasColumnX = infoOffsetX + 18f * scale; + ImGui.SetCursorPos(new Vector2(aliasColumnX, infoStartY)); + + ImGui.BeginGroup(); + using (_uiSharedService.UidFont.Push()) + { + ImGui.TextUnformatted(primaryHeaderText); + } + + foreach (var (text, useColor, disabled) in secondaryHeaderLines) + { + if (useColor && useVanityColors) + { + var seString = SeStringUtils.BuildFormattedPlayerName(text, vanityTextColor, vanityGlowColor); + SeStringUtils.RenderSeStringWithHitbox(seString, ImGui.GetCursorScreenPos(), ImGui.GetFont()); + } + else + { + if (disabled) + ImGui.TextDisabled(text); + else + ImGui.TextUnformatted(text); + } + } + ImGui.EndGroup(); + var namesEnd = ImGui.GetCursorPos(); + + var aliasGroupRectMin = ImGui.GetItemRectMin(); + var aliasGroupRectMax = ImGui.GetItemRectMax(); + var aliasGroupLocalMin = aliasGroupRectMin - windowPos; + var aliasGroupLocalMax = aliasGroupRectMax - windowPos; + + var tagsStartLocal = new Vector2(aliasGroupLocalMax.X + style.ItemSpacing.X + 25f * scale, aliasGroupLocalMin.Y + style.FramePadding.Y + 2f * scale); + ImGui.SetCursorPos(tagsStartLocal); + if (profileTags.Count > 0) + RenderProfileTags(profileTags, scale); + else + ImGui.TextDisabled("-- No tags set --"); + var tagsEndLocal = ImGui.GetCursorPos(); + var tagsBlockBottom = windowPos.Y + tagsEndLocal.Y; + var aliasBlockBottom = windowPos.Y + aliasGroupLocalMax.Y; + var aliasAndTagsBottomLocal = MathF.Max(aliasGroupLocalMax.Y, tagsEndLocal.Y); + var aliasAndTagsBlockBottom = MathF.Max(aliasBlockBottom, tagsBlockBottom); + + var descriptionSeparatorSpacing = style.ItemSpacing.Y * 0.35f; + var descriptionSeparatorThickness = MathF.Max(1f, scale); + var descriptionExtraOffset = 0f; + if (groupInfo?.Owner is not null) + descriptionExtraOffset = style.ItemSpacing.Y * 0.6f; + var descriptionStartLocal = new Vector2(aliasColumnX, aliasAndTagsBottomLocal + descriptionSeparatorSpacing + descriptionExtraOffset); + var horizontalInset = style.ItemSpacing.X * 0.5f; + var descriptionSeparatorStart = windowPos + new Vector2(aliasColumnX - horizontalInset, descriptionStartLocal.Y); + var descriptionSeparatorEnd = new Vector2(windowPos.X + windowSize.X - style.WindowPadding.X + horizontalInset, descriptionSeparatorStart.Y); + drawList.AddLine(descriptionSeparatorStart, descriptionSeparatorEnd, ImGui.GetColorU32(portraitFrameBorder), descriptionSeparatorThickness); + + var descriptionContentStartLocal = new Vector2(aliasColumnX, descriptionStartLocal.Y + descriptionSeparatorThickness + descriptionSeparatorSpacing + style.FramePadding.Y * 0.75f); + ImGui.SetCursorPos(descriptionContentStartLocal); + ImGui.TextDisabled("Description"); + ImGui.SetCursorPosX(aliasColumnX); + var descriptionRegionWidth = ImGui.GetContentRegionAvail().X; + if (descriptionRegionWidth <= 0f) + descriptionRegionWidth = 1f; + var measurementWrapWidth = MathF.Max(1f, descriptionRegionWidth - style.WindowPadding.X * 2f); + var hasDescription = !string.IsNullOrWhiteSpace(profile.Description); + float descriptionContentHeight; + float lineHeightWithSpacing; + using (_uiSharedService.GameFont.Push()) + { + lineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing(); + var measurementText = hasDescription + ? NormalizeDescriptionForMeasurement(profile.Description!) + : GroupDescriptionPlaceholder; + if (string.IsNullOrWhiteSpace(measurementText)) + measurementText = GroupDescriptionPlaceholder; + + descriptionContentHeight = ImGui.CalcTextSize(measurementText, wrapWidth: measurementWrapWidth).Y; + if (descriptionContentHeight <= 0f) + descriptionContentHeight = lineHeightWithSpacing; + } + + var maxDescriptionHeight = lineHeightWithSpacing * DescriptionMaxVisibleLines; + var descriptionChildHeight = Math.Clamp(descriptionContentHeight, lineHeightWithSpacing, maxDescriptionHeight); + + RenderDescriptionChild( + "##StandaloneGroupDescription", + new Vector2(descriptionRegionWidth, descriptionChildHeight), + hasDescription ? profile.Description : null, + GroupDescriptionPlaceholder); + var descriptionEndLocal = ImGui.GetCursorPos(); + var descriptionBlockBottom = windowPos.Y + descriptionEndLocal.Y; + aliasAndTagsBottomLocal = MathF.Max(aliasAndTagsBottomLocal, descriptionEndLocal.Y); + aliasAndTagsBlockBottom = MathF.Max(aliasAndTagsBlockBottom, descriptionBlockBottom); + + var presenceLabelSpacing = style.ItemSpacing.Y * 0.35f; + var presenceAnchorY = MathF.Max(portraitFrameLocalMax.Y, aliasGroupLocalMax.Y); + var presenceStartLocal = new Vector2(portraitFrameLocalMin.X, presenceAnchorY + presenceLabelSpacing); + ImGui.SetCursorPos(presenceStartLocal); + ImGui.TextDisabled("Presence"); + ImGui.SetCursorPosX(portraitFrameLocalMin.X); + if (presenceTokens.Count > 0) + { + var presenceColumnWidth = MathF.Max(1f, aliasColumnX - portraitFrameLocalMin.X - style.ItemSpacing.X); + RenderPresenceTokens(presenceTokens, scale, presenceColumnWidth); + } + else + { + ImGui.TextDisabled("-- No status flags --"); + ImGui.Dummy(new Vector2(0f, style.ItemSpacing.Y * 0.25f)); + } + + var presenceContentEnd = ImGui.GetCursorPos(); + var separatorSpacing = style.ItemSpacing.Y * 0.2f; + var separatorThickness = MathF.Max(1f, scale); + var separatorStartLocal = new Vector2(portraitFrameLocalMin.X, presenceContentEnd.Y + separatorSpacing); + var separatorStart = windowPos + separatorStartLocal; + var separatorEnd = new Vector2(portraitFrameMax.X, separatorStart.Y); + drawList.AddLine(separatorStart, separatorEnd, ImGui.GetColorU32(portraitFrameBorder), separatorThickness); + var afterSeparatorLocal = separatorStartLocal + new Vector2(0f, separatorThickness + separatorSpacing * 0.75f); + + var columnStartLocalY = afterSeparatorLocal.Y; + var leftColumnX = portraitFrameLocalMin.X; + ImGui.SetCursorPos(new Vector2(leftColumnX, columnStartLocalY)); + float leftColumnEndY = columnStartLocalY; + + if (!string.IsNullOrEmpty(noteText)) + { + ImGui.TextDisabled("Notes"); + ImGui.SetCursorPosX(leftColumnX); + ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X); + ImGui.TextUnformatted(noteText); + ImGui.PopTextWrapPos(); + ImGui.SetCursorPos(new Vector2(leftColumnX, ImGui.GetCursorPosY() + style.ItemSpacing.Y * 0.5f)); + leftColumnEndY = ImGui.GetCursorPosY(); + } + + leftColumnEndY = MathF.Max(leftColumnEndY, ImGui.GetCursorPosY()); + + var columnsBottomLocal = leftColumnEndY; + var columnsBottom = windowPos.Y + columnsBottomLocal; + var topAreaBase = windowPos.Y + topAreaStart.Y; + var contentBlockBottom = MathF.Max(columnsBottom, aliasAndTagsBlockBottom); + var leftBlockBottom = MathF.Max(portraitBlockBottom, contentBlockBottom); + var topAreaHeight = leftBlockBottom - topAreaBase; + if (topAreaHeight < 0f) + topAreaHeight = 0f; + + ImGui.SetCursorPos(new Vector2(leftColumnX, topAreaStart.Y + topAreaHeight + style.ItemSpacing.Y)); + + var finalCursorY = ImGui.GetCursorPosY(); + var paddingY = ImGui.GetStyle().WindowPadding.Y; + var computedHeight = finalCursorY + paddingY; + var adjustedHeight = Math.Clamp(computedHeight, minHeight, maxAllowedHeight); + _lastComputedWindowHeight = adjustedHeight; + + var finalSize = new Vector2(baseWidth, adjustedHeight); + Size = finalSize; + ImGui.SetWindowSize(finalSize, ImGuiCond.Always); + } + + private IDalamudTextureWrap? GetBannerTexture(byte[] bannerBytes) + { + if (_bannerTextureLoaded) + return _bannerTextureWrap; + + _bannerTextureLoaded = true; + + if (bannerBytes.Length == 0) + return null; + + _bannerTextureWrap = _uiSharedService.LoadImage(bannerBytes); + return _bannerTextureWrap; + } + + private void ResetBannerTexture() + { + _bannerTextureWrap?.Dispose(); + _bannerTextureWrap = null; + _lastBannerPicture = []; + _bannerTextureLoaded = false; + } + + private static bool IsWindowBeingDragged() + { + return ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows) && ImGui.GetIO().MouseDown[0]; + } + + private static string NormalizeDescriptionForMeasurement(string description) + { + if (string.IsNullOrWhiteSpace(description)) + return string.Empty; + + var normalized = description.ReplaceLineEndings("\n"); + normalized = normalized + .Replace("
", "\n", StringComparison.OrdinalIgnoreCase) + .Replace("
", "\n", StringComparison.OrdinalIgnoreCase) + .Replace("
", "\n", StringComparison.OrdinalIgnoreCase) + .Replace("
", "\n", StringComparison.OrdinalIgnoreCase); + + return SeStringUtils.StripMarkup(normalized); + } + private static void RenderPresenceTokens(IReadOnlyList tokens, float scale, float? maxWidth = null) + { + if (tokens.Count == 0) + return; + + var drawList = ImGui.GetWindowDrawList(); + var style = ImGui.GetStyle(); + + var startPos = ImGui.GetCursorPos(); + float startX = startPos.X; + float cursorX = startX; + float cursorY = startPos.Y; + float availWidth = maxWidth ?? ImGui.GetContentRegionAvail().X; + if (availWidth <= 0f) + availWidth = ImGui.GetContentRegionAvail().X; + if (availWidth <= 0f) + availWidth = 1f; + float spacingX = style.ItemSpacing.X; + float spacingY = style.ItemSpacing.Y; + float rounding = style.FrameRounding > 0f ? style.FrameRounding : 6f * scale; + + var padding = new Vector2(8f * scale, 4f * scale); + var baseColor = new Vector4(0.16f, 0.16f, 0.16f, 0.95f); + var baseBorder = style.Colors[(int)ImGuiCol.Border]; + baseBorder.W *= 0.35f; + var alertColor = new Vector4(0.32f, 0.2f, 0.2f, 0.95f); + var alertBorder = Vector4.Lerp(baseBorder, new Vector4(0.9f, 0.5f, 0.5f, baseBorder.W), 0.6f); + var textColor = style.Colors[(int)ImGuiCol.Text]; + + float rowHeight = 0f; + + for (int i = 0; i < tokens.Count; i++) + { + var token = tokens[i]; + var textSize = ImGui.CalcTextSize(token.Text); + var tagSize = textSize + padding * 2f; + + if (cursorX > startX && cursorX + tagSize.X > startX + availWidth) + { + cursorX = startX; + cursorY += rowHeight + spacingY; + rowHeight = 0f; + } + + ImGui.SetCursorPos(new Vector2(cursorX, cursorY)); + ImGui.InvisibleButton($"##presenceTag_{i}", tagSize); + + var tagMin = ImGui.GetItemRectMin(); + var tagMax = ImGui.GetItemRectMax(); + + var fillColor = token.Emphasis ? alertColor : baseColor; + var borderColor = token.Emphasis ? alertBorder : baseBorder; + + drawList.AddRectFilled(tagMin, tagMax, ImGui.GetColorU32(fillColor), rounding); + drawList.AddRect(tagMin, tagMax, ImGui.GetColorU32(borderColor), rounding); + drawList.AddText(tagMin + padding, ImGui.GetColorU32(textColor), token.Text); + + if (token.Tooltip is { Count: > 0 }) + { + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + if (!string.IsNullOrEmpty(token.TooltipTitle)) + { + ImGui.TextUnformatted(token.TooltipTitle); + ImGui.Separator(); + } + + foreach (var line in token.Tooltip) + ImGui.TextUnformatted(line); + ImGui.EndTooltip(); + } + } + + cursorX += tagSize.X + spacingX; + rowHeight = MathF.Max(rowHeight, tagSize.Y); + } + + ImGui.SetCursorPos(new Vector2(startX, cursorY + rowHeight)); + ImGui.Dummy(new Vector2(0f, spacingY * 0.25f)); + } + public override void OnClose() { + if (!_isGroupProfile + && !_isLightfinderContext + && Pair is null + && _userData is not null + && ProfileEditorLayoutCoordinator.IsActive(_userData.UID)) + { + ProfileEditorLayoutCoordinator.Disable(_userData.UID); + } + else if (_isGroupProfile + && _groupData is not null + && ProfileEditorLayoutCoordinator.IsActive(_groupData.GID)) + { + ProfileEditorLayoutCoordinator.Disable(_groupData.GID); + } Mediator.Publish(new RemoveWindowMessage(this)); } -} \ No newline at end of file + + private readonly record struct PresenceToken( + string Text, + bool Emphasis, + IReadOnlyList? Tooltip = null, + string? TooltipTitle = null); +} diff --git a/LightlessSync/UI/Style/AnimatedHeader.cs b/LightlessSync/UI/Style/AnimatedHeader.cs new file mode 100644 index 0000000..0037b53 --- /dev/null +++ b/LightlessSync/UI/Style/AnimatedHeader.cs @@ -0,0 +1,463 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using System.Numerics; + +namespace LightlessSync.UI.Style; + +/// +/// A reusable animated header component with a gradient background, some funny stars, and shooting star effects to match the lightless void theme a bit. +/// +public class AnimatedHeader +{ + private struct Particle + { + public Vector2 Position; + public Vector2 Velocity; + public float Life; + public float MaxLife; + public float Size; + public ParticleType Type; + public List? Trail; + public float Twinkle; + public float Depth; + public float Hue; + } + + private enum ParticleType + { + TwinklingStar, + ShootingStar + } + + private readonly List _particles = []; + private float _particleSpawnTimer; + private readonly Random _random = new(); + + private const float _particleSpawnInterval = 0.2f; + private const int _maxParticles = 50; + private const int _maxTrailLength = 50; + private const float _edgeFadeDistance = 30f; + private const float _extendedParticleHeight = 40f; + + public float Height { get; set; } = 150f; + public Vector4 TopColor { get; set; } = new(0.08f, 0.05f, 0.15f, 1.0f); + public Vector4 BottomColor { get; set; } = new(0.12f, 0.08f, 0.20f, 1.0f); + public bool EnableParticles { get; set; } = true; + public bool EnableBottomGradient { get; set; } = true; + + /// + /// Draws the animated header with some customizable content + /// + /// Width of the header + /// Action to draw custom content inside the header + public void Draw(float width, Action drawContent) + { + var windowPos = ImGui.GetWindowPos(); + var windowPadding = ImGui.GetStyle().WindowPadding; + + var headerStart = windowPos + new Vector2(windowPadding.X, windowPadding.Y); + var headerEnd = headerStart + new Vector2(width, Height); + var extendedParticleSize = new Vector2(width, Height + _extendedParticleHeight); + + DrawGradientBackground(headerStart, headerEnd); + + if (EnableParticles) + { + DrawParticleEffects(headerStart, extendedParticleSize); + } + + drawContent(headerStart, headerEnd); + + if (EnableBottomGradient) + { + DrawBottomGradient(headerStart, headerEnd, width); + } + } + + /// + /// Draws a simple animated header with title and subtitle. + /// + public void DrawSimple(float width, string title, string subtitle, IFontHandle? titleFont = null, Vector4? titleColor = null, Vector4? subtitleColor = null) + { + Draw(width, (headerStart, headerEnd) => + { + var textX = 20f; + var textY = 30f; + + ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY)); + + if (titleFont != null) + { + using (titleFont.Push()) + { + ImGui.TextColored(titleColor ?? new Vector4(0.95f, 0.95f, 0.95f, 1.0f), title); + } + } + else + { + ImGui.TextColored(titleColor ?? new Vector4(0.95f, 0.95f, 0.95f, 1.0f), title); + } + + ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f)); + ImGui.TextColored(subtitleColor ?? UIColors.Get("LightlessBlue"), subtitle); + }); + } + + /// + /// Draws a header with title, subtitle, and action buttons in the top-right corner. + /// + public void DrawWithButtons(float width, string title, string subtitle, List buttons, IFontHandle? titleFont = null) + { + Draw(width, (headerStart, headerEnd) => + { + // Draw title and subtitle + var textX = 20f; + var textY = 30f; + + ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY)); + + if (titleFont != null) + { + using (titleFont.Push()) + { + ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), title); + } + } + else + { + ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), title); + } + + ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f)); + ImGui.TextColored(UIColors.Get("LightlessBlue"), subtitle); + + // Draw buttons + if (buttons.Count > 0) + { + DrawHeaderButtons(headerStart, width, buttons); + } + }); + } + + private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd) + { + var drawList = ImGui.GetWindowDrawList(); + + drawList.AddRectFilledMultiColor( + headerStart, + headerEnd, + ImGui.GetColorU32(TopColor), + ImGui.GetColorU32(TopColor), + ImGui.GetColorU32(BottomColor), + ImGui.GetColorU32(BottomColor) + ); + + // Draw static background stars + var random = new Random(42); + for (int i = 0; i < 50; i++) + { + var starPos = headerStart + new Vector2( + (float)random.NextDouble() * (headerEnd.X - headerStart.X), + (float)random.NextDouble() * (headerEnd.Y - headerStart.Y) + ); + var brightness = 0.3f + (float)random.NextDouble() * 0.4f; + drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(new Vector4(1f, 1f, 1f, brightness))); + } + } + + private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width) + { + var drawList = ImGui.GetWindowDrawList(); + var gradientHeight = 60f; + + for (int i = 0; i < gradientHeight; i++) + { + var progress = i / gradientHeight; + var smoothProgress = progress * progress; + var r = BottomColor.X + (0.0f - BottomColor.X) * smoothProgress; + var g = BottomColor.Y + (0.0f - BottomColor.Y) * smoothProgress; + var b = BottomColor.Z + (0.0f - BottomColor.Z) * smoothProgress; + var alpha = 1f - smoothProgress; + var gradientColor = new Vector4(r, g, b, alpha); + drawList.AddLine( + new Vector2(headerStart.X, headerEnd.Y + i), + new Vector2(headerStart.X + width, headerEnd.Y + i), + ImGui.GetColorU32(gradientColor), + 1f + ); + } + } + + private void DrawHeaderButtons(Vector2 headerStart, float headerWidth, List buttons) + { + var spacing = 8f * ImGuiHelpers.GlobalScale; + var rightPadding = 15f * ImGuiHelpers.GlobalScale; + var topPadding = 15f * ImGuiHelpers.GlobalScale; + var buttonY = headerStart.Y + topPadding; + + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + // Calculate button size (assuming all buttons are the same size) + var buttonSize = ImGui.CalcTextSize(FontAwesomeIcon.Globe.ToIconString()); + buttonSize += ImGui.GetStyle().FramePadding * 2; + + float currentX = headerStart.X + headerWidth - rightPadding - buttonSize.X; + + using (ImRaii.PushColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0))) + using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple") with { W = 0.3f })) + using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive") with { W = 0.5f })) + { + for (int i = buttons.Count - 1; i >= 0; i--) + { + var button = buttons[i]; + ImGui.SetCursorScreenPos(new Vector2(currentX, buttonY)); + + if (ImGui.Button(button.Icon.ToIconString())) + { + button.OnClick?.Invoke(); + } + + if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(button.Tooltip)) + { + ImGui.SetTooltip(button.Tooltip); + } + + currentX -= buttonSize.X + spacing; + } + } + } + } + + private void DrawParticleEffects(Vector2 bannerStart, Vector2 bannerSize) + { + var deltaTime = ImGui.GetIO().DeltaTime; + _particleSpawnTimer += deltaTime; + + if (_particleSpawnTimer > _particleSpawnInterval && _particles.Count < _maxParticles) + { + SpawnParticle(bannerSize); + _particleSpawnTimer = 0f; + } + + if (_random.NextDouble() < 0.003) + { + SpawnShootingStar(bannerSize); + } + + var drawList = ImGui.GetWindowDrawList(); + + for (int i = _particles.Count - 1; i >= 0; i--) + { + var particle = _particles[i]; + + var screenPos = bannerStart + particle.Position; + + if (particle.Type == ParticleType.ShootingStar && particle.Trail != null) + { + particle.Trail.Insert(0, particle.Position); + if (particle.Trail.Count > _maxTrailLength) + particle.Trail.RemoveAt(particle.Trail.Count - 1); + } + + if (particle.Type == ParticleType.TwinklingStar) + { + particle.Twinkle += 0.005f * particle.Depth; + } + + particle.Position += particle.Velocity * deltaTime; + particle.Life -= deltaTime; + + var isOutOfBounds = particle.Position.X < -50 || particle.Position.X > bannerSize.X + 50 || + particle.Position.Y < -50 || particle.Position.Y > bannerSize.Y + 50; + + if (particle.Life <= 0 || (particle.Type != ParticleType.TwinklingStar && isOutOfBounds)) + { + _particles.RemoveAt(i); + continue; + } + + if (particle.Type == ParticleType.TwinklingStar) + { + if (particle.Position.X < 0 || particle.Position.X > bannerSize.X) + particle.Velocity = particle.Velocity with { X = -particle.Velocity.X }; + if (particle.Position.Y < 0 || particle.Position.Y > bannerSize.Y) + particle.Velocity = particle.Velocity with { Y = -particle.Velocity.Y }; + } + + var fadeIn = Math.Min(1f, (particle.MaxLife - particle.Life) / 20f); + var fadeOut = Math.Min(1f, particle.Life / 20f); + var lifeFade = Math.Min(fadeIn, fadeOut); + + var edgeFadeX = Math.Min( + Math.Min(1f, (particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance), + Math.Min(1f, (bannerSize.X - particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance) + ); + var edgeFadeY = Math.Min( + Math.Min(1f, (particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance), + Math.Min(1f, (bannerSize.Y - particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance) + ); + var edgeFade = Math.Min(edgeFadeX, edgeFadeY); + + var baseAlpha = lifeFade * edgeFade; + var finalAlpha = particle.Type == ParticleType.TwinklingStar + ? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle)) + : baseAlpha; + + if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1) + { + var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f); + + for (int t = 1; t < particle.Trail.Count; t++) + { + var trailProgress = (float)t / particle.Trail.Count; + var trailAlpha = Math.Min(1f, (1f - trailProgress) * finalAlpha * 1.8f); + var trailWidth = (1f - trailProgress) * 3f + 1f; + + var glowAlpha = trailAlpha * 0.4f; + drawList.AddLine( + bannerStart + particle.Trail[t - 1], + bannerStart + particle.Trail[t], + ImGui.GetColorU32(cyanColor with { W = glowAlpha }), + trailWidth + 4f + ); + + drawList.AddLine( + bannerStart + particle.Trail[t - 1], + bannerStart + particle.Trail[t], + ImGui.GetColorU32(cyanColor with { W = trailAlpha }), + trailWidth + ); + } + } + else if (particle.Type == ParticleType.TwinklingStar) + { + DrawTwinklingStar(drawList, screenPos, particle.Size, particle.Hue, finalAlpha, particle.Depth); + } + + _particles[i] = particle; + } + } + + private static void DrawTwinklingStar(ImDrawListPtr drawList, Vector2 position, float size, float hue, float alpha, float depth) + { + var color = HslToRgb(hue, 1.0f, 0.85f); + color.W = alpha; + + drawList.AddCircleFilled(position, size, ImGui.GetColorU32(color)); + + var glowColor = color with { W = alpha * 0.3f }; + drawList.AddCircleFilled(position, size * (1.2f + depth * 0.3f), ImGui.GetColorU32(glowColor)); + } + + private static Vector4 HslToRgb(float h, float s, float l) + { + h = h / 360f; + float c = (1 - MathF.Abs(2 * l - 1)) * s; + float x = c * (1 - MathF.Abs((h * 6) % 2 - 1)); + float m = l - c / 2; + + float r, g, b; + if (h < 1f / 6f) + { + r = c; g = x; b = 0; + } + else if (h < 2f / 6f) + { + r = x; g = c; b = 0; + } + else if (h < 3f / 6f) + { + r = 0; g = c; b = x; + } + else if (h < 4f / 6f) + { + r = 0; g = x; b = c; + } + else if (h < 5f / 6f) + { + r = x; g = 0; b = c; + } + else + { + r = c; g = 0; b = x; + } + + return new Vector4(r + m, g + m, b + m, 1.0f); + } + + private void SpawnParticle(Vector2 bannerSize) + { + var position = new Vector2( + (float)_random.NextDouble() * bannerSize.X, + (float)_random.NextDouble() * bannerSize.Y + ); + + var depthLayers = new[] { 0.5f, 1.0f, 1.5f }; + var depth = depthLayers[_random.Next(depthLayers.Length)]; + + var velocity = new Vector2( + ((float)_random.NextDouble() - 0.5f) * 0.05f * depth, + ((float)_random.NextDouble() - 0.5f) * 0.05f * depth + ); + + var isBlue = _random.NextDouble() < 0.5; + var hue = isBlue ? 220f + (float)_random.NextDouble() * 30f : 270f + (float)_random.NextDouble() * 40f; + var size = (0.5f + (float)_random.NextDouble() * 2f) * depth; + var maxLife = 120f + (float)_random.NextDouble() * 60f; + + _particles.Add(new Particle + { + Position = position, + Velocity = velocity, + Life = maxLife, + MaxLife = maxLife, + Size = size, + Type = ParticleType.TwinklingStar, + Trail = null, + Twinkle = (float)_random.NextDouble() * MathF.PI * 2, + Depth = depth, + Hue = hue + }); + } + + private void SpawnShootingStar(Vector2 bannerSize) + { + var maxLife = 80f + (float)_random.NextDouble() * 40f; + var startX = bannerSize.X * (0.3f + (float)_random.NextDouble() * 0.6f); + var startY = -10f; + + _particles.Add(new Particle + { + Position = new Vector2(startX, startY), + Velocity = new Vector2( + -50f - (float)_random.NextDouble() * 40f, + 30f + (float)_random.NextDouble() * 40f + ), + Life = maxLife, + MaxLife = maxLife, + Size = 2.5f, + Type = ParticleType.ShootingStar, + Trail = new List(), + Twinkle = 0, + Depth = 1.0f, + Hue = 270f + }); + } + + /// + /// Clears all active particles. Useful when closing or hiding a window with an animated header. + /// + public void ClearParticles() + { + _particles.Clear(); + _particleSpawnTimer = 0f; + } +} + +/// +/// Represents a button in the animated header. +/// +public record HeaderButton(FontAwesomeIcon Icon, string Tooltip, Action? OnClick = null); diff --git a/LightlessSync/UI/Style/MainStyle.cs b/LightlessSync/UI/Style/MainStyle.cs index d3d8b68..3da7455 100644 --- a/LightlessSync/UI/Style/MainStyle.cs +++ b/LightlessSync/UI/Style/MainStyle.cs @@ -38,7 +38,7 @@ internal static class MainStyle new("color.border", "Border", () => Rgba(65, 65, 65, 255), ImGuiCol.Border), new("color.borderShadow", "Border Shadow", () => Rgba(0, 0, 0, 150), ImGuiCol.BorderShadow), new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg), - new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 255), ImGuiCol.FrameBgHovered), + new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 100), ImGuiCol.FrameBgHovered), new("color.frameBgActive", "Frame Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.FrameBgActive), new("color.titleBg", "Title Background", () => Rgba(24, 24, 24, 232), ImGuiCol.TitleBg), new("color.titleBgActive", "Title Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.TitleBgActive), diff --git a/LightlessSync/UI/Style/Selune.cs b/LightlessSync/UI/Style/Selune.cs new file mode 100644 index 0000000..f89a1f0 --- /dev/null +++ b/LightlessSync/UI/Style/Selune.cs @@ -0,0 +1,1006 @@ +using System; +using System.Numerics; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using LightlessSync.UI; + +// imagine putting this goober name here + +namespace LightlessSync.UI.Style; + +public enum SeluneGradientMode +{ + Vertical, + Horizontal, + Both, +} + +public enum SeluneHighlightMode +{ + Horizontal, + Vertical, + Both, + Point, +} + +public sealed class SeluneGradientSettings +{ + public Vector4 GradientColor { get; init; } = UIColors.Get("LightlessPurple"); + public Vector4? HighlightColor { get; init; } + public float GradientPeakOpacity { get; init; } = 0.07f; + public float HighlightPeakAlpha { get; init; } = 0.13f; + public float HighlightEdgeAlpha { get; init; } = 0f; + public float HighlightMidpoint { get; init; } = 0.45f; + public float MinimumHighlightHalfHeight { get; init; } = 25f; + public float MinimumHighlightHalfWidth { get; init; } = 25f; + public float HighlightFadeInSpeed { get; init; } = 14f; + public float HighlightFadeOutSpeed { get; init; } = 8f; + public float HighlightBorderThickness { get; init; } = 10f; + public float HighlightBorderRounding { get; init; } = 8f; + public SeluneGradientMode BackgroundMode { get; init; } = SeluneGradientMode.Vertical; + public SeluneHighlightMode HighlightMode { get; init; } = SeluneHighlightMode.Horizontal; + + public static SeluneGradientSettings Default + => new() + { + GradientColor = UIColors.Get("LightlessPurple"), + HighlightColor = UIColors.Get("LightlessPurple"), + }; +} + +public static class Selune +{ + [ThreadStatic] private static SeluneCanvas? _activeCanvas; + + public static SeluneCanvas Begin(SeluneBrush brush, ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, SeluneGradientSettings? settings = null) + { + var canvas = new SeluneCanvas(brush, drawList, windowPos, windowSize, settings ?? SeluneGradientSettings.Default, _activeCanvas); + _activeCanvas = canvas; + return canvas; + } + + internal static void Release(SeluneCanvas canvas) + { + if (_activeCanvas == canvas) + _activeCanvas = canvas.Previous; + } + + public static void RegisterHighlight( + Vector2 rectMin, + Vector2 rectMax, + SeluneHighlightMode? modeOverride = null, + bool borderOnly = false, + float? borderThicknessOverride = null, + bool exactSize = false, + bool clipToElement = false, + Vector2? clipPadding = null, + float? roundingOverride = null, + bool spanFullWidth = false, + Vector4? highlightColorOverride = null, + float? highlightAlphaOverride = null) + => _activeCanvas?.RegisterHighlight( + rectMin, + rectMax, + modeOverride, + borderOnly, + borderThicknessOverride, + exactSize, + clipToElement, + clipPadding, + roundingOverride, + spanFullWidth, + highlightColorOverride, + highlightAlphaOverride); +} + +public sealed class SeluneBrush +{ + private Vector2? _highlightCenter; + private Vector2 _highlightHalfSize; + private SeluneHighlightMode _highlightMode; + private bool _highlightBorderOnly; + private float _borderThickness; + private bool _useClipRect; + private Vector2 _clipMin; + private Vector2 _clipMax; + private float _highlightRounding; + private bool _highlightUsedThisFrame; + private float _highlightIntensity; + private Vector4? _highlightColorOverride; + private float? _highlightAlphaOverride; + + internal void BeginFrame() + { + _highlightUsedThisFrame = false; + } + + internal void RegisterHighlight(Vector2 center, Vector2 halfSize, SeluneHighlightMode mode, bool borderOnly, float borderThickness, bool useClipRect, Vector2 clipMin, Vector2 clipMax, float rounding, Vector4? highlightColorOverride, float? highlightAlphaOverride) + { + if (halfSize.X <= 0f || halfSize.Y <= 0f) + return; + + _highlightUsedThisFrame = true; + _highlightCenter = center; + _highlightHalfSize = halfSize; + _highlightMode = mode; + _highlightBorderOnly = borderOnly; + _borderThickness = borderOnly ? Math.Max(borderThickness, 0f) : 0f; + _useClipRect = useClipRect; + _clipMin = clipMin; + _clipMax = clipMax; + _highlightRounding = rounding; + _highlightColorOverride = highlightColorOverride; + _highlightAlphaOverride = highlightAlphaOverride; + } + + internal void UpdateFade(float deltaTime, SeluneGradientSettings settings) + { + if (deltaTime <= 0f) + return; + + if (_highlightUsedThisFrame) + { + _highlightIntensity = MathF.Min(1f, _highlightIntensity + deltaTime * settings.HighlightFadeInSpeed); + } + else + { + _highlightIntensity = MathF.Max(0f, _highlightIntensity - deltaTime * settings.HighlightFadeOutSpeed); + + if (_highlightIntensity <= 0.001f) + { + ResetHighlightState(); + } + } + } + + internal SeluneHighlightRenderState GetRenderState() + => new( + _highlightCenter, + _highlightHalfSize, + _highlightMode, + _highlightBorderOnly, + _borderThickness, + _useClipRect, + _clipMin, + _clipMax, + _highlightRounding, + _highlightIntensity, + _highlightColorOverride, + _highlightAlphaOverride); + + private void ResetHighlightState() + { + _highlightCenter = null; + _highlightHalfSize = Vector2.Zero; + _highlightBorderOnly = false; + _borderThickness = 0f; + _useClipRect = false; + _highlightRounding = 0f; + _highlightColorOverride = null; + _highlightAlphaOverride = null; + } +} + +internal readonly struct SeluneHighlightRenderState +{ + public SeluneHighlightRenderState( + Vector2? center, + Vector2 halfSize, + SeluneHighlightMode mode, + bool borderOnly, + float borderThickness, + bool useClipRect, + Vector2 clipMin, + Vector2 clipMax, + float rounding, + float intensity, + Vector4? colorOverride, + float? alphaOverride) + { + Center = center; + HalfSize = halfSize; + Mode = mode; + BorderOnly = borderOnly; + BorderThickness = borderThickness; + UseClipRect = useClipRect; + ClipMin = clipMin; + ClipMax = clipMax; + Rounding = rounding; + Intensity = intensity; + ColorOverride = colorOverride; + AlphaOverride = alphaOverride; + } + + public Vector2? Center { get; } + public Vector2 HalfSize { get; } + public SeluneHighlightMode Mode { get; } + public bool BorderOnly { get; } + public float BorderThickness { get; } + public bool UseClipRect { get; } + public Vector2 ClipMin { get; } + public Vector2 ClipMax { get; } + public float Rounding { get; } + public float Intensity { get; } + public Vector4? ColorOverride { get; } + public float? AlphaOverride { get; } + + public bool HasHighlight => Center.HasValue && HalfSize.X > 0f && HalfSize.Y > 0f && Intensity > 0.001f; +} + +public sealed class SeluneCanvas : IDisposable +{ + private readonly SeluneBrush _brush; + private readonly ImDrawListPtr _drawList; + private readonly Vector2 _windowPos; + private readonly Vector2 _windowSize; + private readonly SeluneGradientSettings _settings; + private bool _fadeUpdatedThisFrame; + + internal SeluneCanvas? Previous { get; } + + internal SeluneCanvas(SeluneBrush brush, ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, SeluneGradientSettings settings, SeluneCanvas? previous) + { + _brush = brush; + _drawList = drawList; + _windowPos = windowPos; + _windowSize = windowSize; + _settings = settings; + Previous = previous; + _fadeUpdatedThisFrame = false; + + _brush.BeginFrame(); + } + + public void DrawGradient(float gradientTopY, float gradientBottomY, float deltaTime) + { + DrawInternal(gradientTopY, gradientBottomY, deltaTime, true, true); + } + + public void DrawHighlightOnly(float gradientTopY, float gradientBottomY, float deltaTime) + { + DrawInternal(gradientTopY, gradientBottomY, deltaTime, false, true); + } + + public void DrawHighlightOnly(float deltaTime) + => DrawHighlightOnly(_windowPos.Y, _windowPos.Y + _windowSize.Y, deltaTime); + + public void Animate(float deltaTime) + { + UpdateFadeOnce(deltaTime); + } + + private void UpdateFadeOnce(float deltaTime) + { + if (_fadeUpdatedThisFrame) + return; + + _brush.UpdateFade(deltaTime, _settings); + _fadeUpdatedThisFrame = true; + } + + private void DrawInternal(float gradientTopY, float gradientBottomY, float deltaTime, bool drawBackground, bool drawHighlight) + { + UpdateFadeOnce(deltaTime); + + SeluneRenderer.DrawGradient( + _drawList, + _windowPos, + _windowSize, + gradientTopY, + gradientBottomY, + _brush.GetRenderState(), + _settings, + drawBackground, + drawHighlight); + } + + internal void RegisterHighlight( + Vector2 rectMin, + Vector2 rectMax, + SeluneHighlightMode? modeOverride, + bool borderOnly, + float? borderThicknessOverride, + bool exactSize, + bool clipToElement, + Vector2? clipPadding, + float? roundingOverride, + bool spanFullWidth, + Vector4? highlightColorOverride, + float? highlightAlphaOverride) + { + if (spanFullWidth) + { + rectMin.X = _windowPos.X; + rectMax.X = _windowPos.X + _windowSize.X; + } + + var size = rectMax - rectMin; + if (size.X <= 0f || size.Y <= 0f) + return; + + var center = rectMin + size * 0.5f; + var halfWidth = exactSize ? size.X * 0.5f : Math.Max(size.X * 0.5f, _settings.MinimumHighlightHalfWidth * ImGuiHelpers.GlobalScale); + var halfHeight = exactSize ? size.Y * 0.5f : Math.Max(size.Y * 0.5f, _settings.MinimumHighlightHalfHeight * ImGuiHelpers.GlobalScale); + var mode = modeOverride ?? _settings.HighlightMode; + var thickness = borderOnly ? (borderThicknessOverride ?? _settings.HighlightBorderThickness) * ImGuiHelpers.GlobalScale : 0f; + var useClip = clipToElement; + var padding = clipPadding ?? (borderOnly && thickness > 0f + ? new Vector2(thickness) + : Vector2.Zero); + var clipMin = rectMin - padding; + var clipMax = rectMax + padding; + var rounding = (roundingOverride ?? _settings.HighlightBorderRounding) * ImGuiHelpers.GlobalScale; + + _brush.RegisterHighlight(center, new Vector2(halfWidth, halfHeight), mode, borderOnly, thickness, useClip, clipMin, clipMax, rounding, highlightColorOverride, highlightAlphaOverride); + _fadeUpdatedThisFrame = false; + } + + public void Dispose() + => Selune.Release(this); +} + // i wonder which sync will copy this shitty code now +internal static class SeluneRenderer +{ + public static void DrawGradient( + ImDrawListPtr drawList, + Vector2 windowPos, + Vector2 windowSize, + float gradientTopY, + float gradientBottomY, + SeluneHighlightRenderState highlightState, + SeluneGradientSettings settings, + bool drawBackground = true, + bool drawHighlight = true) + { + var gradientLeft = windowPos.X; + var gradientRight = windowPos.X + windowSize.X; + var windowBottomY = windowPos.Y + windowSize.Y; + var clampedTopY = MathF.Max(gradientTopY, windowPos.Y); + var clampedBottomY = MathF.Min(gradientBottomY, windowBottomY); + + if (clampedBottomY <= clampedTopY) + return; + + var color = settings.GradientColor; + var topColorVec = new Vector4(color.X, color.Y, color.Z, 0f); + var bottomColorVec = new Vector4(color.X, color.Y, color.Z, 0f); + var midColorVec = new Vector4(color.X, color.Y, color.Z, settings.GradientPeakOpacity); + + if (drawBackground) + { + DrawBackground( + drawList, + gradientLeft, + gradientRight, + clampedTopY, + clampedBottomY, + topColorVec, + midColorVec, + bottomColorVec, + settings.BackgroundMode); + } + + if (!drawHighlight) + return; + + DrawHighlight( + drawList, + gradientLeft, + gradientRight, + clampedTopY, + clampedBottomY, + highlightState, + settings); + } + + private static void DrawBackground( + ImDrawListPtr drawList, + float gradientLeft, + float gradientRight, + float clampedTopY, + float clampedBottomY, + Vector4 topColorVec, + Vector4 midColorVec, + Vector4 bottomColorVec, + SeluneGradientMode mode) + { + switch (mode) + { + case SeluneGradientMode.Vertical: + DrawVerticalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec); + break; + case SeluneGradientMode.Horizontal: + DrawHorizontalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec); + break; + case SeluneGradientMode.Both: + DrawVerticalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec); + DrawHorizontalBackground(drawList, gradientLeft, gradientRight, clampedTopY, clampedBottomY, topColorVec, midColorVec, bottomColorVec); + break; + } + } + + private static void DrawVerticalBackground( + ImDrawListPtr drawList, + float gradientLeft, + float gradientRight, + float clampedTopY, + float clampedBottomY, + Vector4 topColorVec, + Vector4 midColorVec, + Vector4 bottomColorVec) + { + var topColor = ImGui.ColorConvertFloat4ToU32(topColorVec); + var midColor = ImGui.ColorConvertFloat4ToU32(midColorVec); + var bottomColor = ImGui.ColorConvertFloat4ToU32(bottomColorVec); + + var midY = clampedTopY + (clampedBottomY - clampedTopY) * 0.035f; + drawList.AddRectFilledMultiColor( + new Vector2(gradientLeft, clampedTopY), + new Vector2(gradientRight, midY), + topColor, + topColor, + midColor, + midColor); + + drawList.AddRectFilledMultiColor( + new Vector2(gradientLeft, midY), + new Vector2(gradientRight, clampedBottomY), + midColor, + midColor, + bottomColor, + bottomColor); + } + + private static void DrawHorizontalBackground( + ImDrawListPtr drawList, + float gradientLeft, + float gradientRight, + float clampedTopY, + float clampedBottomY, + Vector4 leftColorVec, + Vector4 midColorVec, + Vector4 rightColorVec) + { + var leftColor = ImGui.ColorConvertFloat4ToU32(leftColorVec); + var midColor = ImGui.ColorConvertFloat4ToU32(midColorVec); + var rightColor = ImGui.ColorConvertFloat4ToU32(rightColorVec); + + var midX = gradientLeft + (gradientRight - gradientLeft) * 0.035f; + drawList.AddRectFilledMultiColor( + new Vector2(gradientLeft, clampedTopY), + new Vector2(midX, clampedBottomY), + leftColor, + midColor, + midColor, + leftColor); + + drawList.AddRectFilledMultiColor( + new Vector2(midX, clampedTopY), + new Vector2(gradientRight, clampedBottomY), + midColor, + rightColor, + rightColor, + midColor); + } + + private static void DrawHighlight( + ImDrawListPtr drawList, + float gradientLeft, + float gradientRight, + float clampedTopY, + float clampedBottomY, + SeluneHighlightRenderState highlightState, + SeluneGradientSettings settings) + { + if (!highlightState.HasHighlight) + return; + + var highlightColor = highlightState.ColorOverride ?? settings.HighlightColor ?? settings.GradientColor; + var clampedIntensity = Math.Clamp(highlightState.Intensity, 0f, 1f); + var alphaScale = Math.Clamp(highlightState.AlphaOverride ?? 1f, 0f, 1f); + var peakAlpha = settings.HighlightPeakAlpha * clampedIntensity * alphaScale; + var edgeAlpha = settings.HighlightEdgeAlpha * clampedIntensity * alphaScale; + + if (peakAlpha <= 0f && edgeAlpha <= 0f) + return; + + var highlightEdgeVec = new Vector4(highlightColor.X, highlightColor.Y, highlightColor.Z, edgeAlpha); + var highlightPeakVec = new Vector4(highlightColor.X, highlightColor.Y, highlightColor.Z, peakAlpha); + var center = highlightState.Center!.Value; + var halfSize = highlightState.HalfSize; + + if (highlightState.UseClipRect) + drawList.PushClipRect(highlightState.ClipMin, highlightState.ClipMax, true); + + switch (highlightState.Mode) + { + case SeluneHighlightMode.Horizontal: + DrawHorizontalHighlight( + drawList, + gradientLeft, + gradientRight, + clampedTopY, + clampedBottomY, + center, + halfSize, + highlightEdgeVec, + highlightPeakVec, + settings.HighlightMidpoint, + highlightState.BorderOnly, + highlightState.BorderThickness, + highlightState.Rounding); + break; + + case SeluneHighlightMode.Vertical: + DrawVerticalHighlight( + drawList, + gradientLeft, + gradientRight, + clampedTopY, + clampedBottomY, + center, + halfSize, + highlightEdgeVec, + highlightPeakVec, + settings.HighlightMidpoint, + highlightState.BorderOnly, + highlightState.BorderThickness, + highlightState.Rounding); + break; + + case SeluneHighlightMode.Both: + DrawCombinedHighlight( + drawList, + gradientLeft, + gradientRight, + clampedTopY, + clampedBottomY, + center, + halfSize, + highlightEdgeVec, + highlightPeakVec, + highlightState.BorderOnly, + highlightState.BorderThickness, + highlightState.Rounding); + break; + + case SeluneHighlightMode.Point: + DrawPointHighlight( + drawList, + center, + halfSize, + highlightEdgeVec, + highlightPeakVec, + highlightState.BorderOnly, + highlightState.BorderThickness); + break; + } + + if (highlightState.UseClipRect) + drawList.PopClipRect(); + } + + private static void DrawHorizontalHighlight( + ImDrawListPtr drawList, + float gradientLeft, + float gradientRight, + float clampedTopY, + float clampedBottomY, + Vector2 center, + Vector2 halfSize, + Vector4 edgeColor, + Vector4 peakColor, + float midpoint, + bool borderOnly, + float borderThickness, + float rounding) + { + var highlightTop = MathF.Max(clampedTopY, center.Y - halfSize.Y); + var highlightBottom = MathF.Min(clampedBottomY, center.Y + halfSize.Y); + if (highlightBottom <= highlightTop) + return; + + var highlightLeft = MathF.Max(gradientLeft, center.X - halfSize.X); + var highlightRight = MathF.Min(gradientRight, center.X + halfSize.X); + + if (highlightRight <= highlightLeft || highlightBottom <= highlightTop) + return; + + if (!borderOnly || borderThickness <= 0f) + { + DrawHorizontalHighlightRect( + drawList, + highlightLeft, + highlightRight, + highlightTop, + highlightBottom, + edgeColor, + peakColor, + midpoint, + 1f); + return; + } + + var innerTop = MathF.Min(highlightBottom, MathF.Max(highlightTop, center.Y - MathF.Max(halfSize.Y - borderThickness, 0f))); + var innerBottom = MathF.Max(highlightTop, MathF.Min(highlightBottom, center.Y + MathF.Max(halfSize.Y - borderThickness, 0f))); + var edgeU32 = ImGui.ColorConvertFloat4ToU32(edgeColor); + var peakU32 = ImGui.ColorConvertFloat4ToU32(peakColor); + + if (innerTop > highlightTop) + { + drawList.AddRectFilledMultiColor( + new Vector2(highlightLeft, highlightTop), + new Vector2(highlightRight, innerTop), + edgeU32, + edgeU32, + peakU32, + peakU32); + } + + if (innerBottom < highlightBottom) + { + drawList.AddRectFilledMultiColor( + new Vector2(highlightLeft, innerBottom), + new Vector2(highlightRight, highlightBottom), + peakU32, + peakU32, + edgeU32, + edgeU32); + } + } + + private static void DrawVerticalHighlight( + ImDrawListPtr drawList, + float gradientLeft, + float gradientRight, + float clampedTopY, + float clampedBottomY, + Vector2 center, + Vector2 halfSize, + Vector4 edgeColor, + Vector4 peakColor, + float midpoint, + bool borderOnly, + float borderThickness, + float rounding) + { + var highlightTop = MathF.Max(clampedTopY, center.Y - halfSize.Y); + var highlightBottom = MathF.Min(clampedBottomY, center.Y + halfSize.Y); + var highlightLeft = MathF.Max(gradientLeft, center.X - halfSize.X); + var highlightRight = MathF.Min(gradientRight, center.X + halfSize.X); + + if (highlightRight <= highlightLeft || highlightBottom <= highlightTop) + return; + + if (!borderOnly || borderThickness <= 0f) + { + DrawVerticalHighlightRect( + drawList, + highlightLeft, + highlightRight, + highlightTop, + highlightBottom, + edgeColor, + peakColor, + midpoint, + 1f); + return; + } + + var innerLeft = MathF.Min(highlightRight, MathF.Max(highlightLeft, center.X - MathF.Max(halfSize.X - borderThickness, 0f))); + var innerRight = MathF.Max(highlightLeft, MathF.Min(highlightRight, center.X + MathF.Max(halfSize.X - borderThickness, 0f))); + var edgeU32 = ImGui.ColorConvertFloat4ToU32(edgeColor); + var peakU32 = ImGui.ColorConvertFloat4ToU32(peakColor); + + if (innerLeft > highlightLeft) + { + drawList.AddRectFilledMultiColor( + new Vector2(highlightLeft, highlightTop), + new Vector2(innerLeft, highlightBottom), + edgeU32, + peakU32, + peakU32, + edgeU32); + } + + if (innerRight < highlightRight) + { + drawList.AddRectFilledMultiColor( + new Vector2(innerRight, highlightTop), + new Vector2(highlightRight, highlightBottom), + peakU32, + edgeU32, + edgeU32, + peakU32); + } + } + + private static void DrawCombinedHighlight( + ImDrawListPtr drawList, + float gradientLeft, + float gradientRight, + float clampedTopY, + float clampedBottomY, + Vector2 center, + Vector2 halfSize, + Vector4 edgeColor, + Vector4 peakColor, + bool borderOnly, + float borderThickness, + float rounding) + { + var highlightLeft = MathF.Max(gradientLeft, center.X - halfSize.X); + var highlightRight = MathF.Min(gradientRight, center.X + halfSize.X); + var highlightTop = MathF.Max(clampedTopY, center.Y - halfSize.Y); + var highlightBottom = MathF.Min(clampedBottomY, center.Y + halfSize.Y); + + if (highlightRight <= highlightLeft || highlightBottom <= highlightTop) + return; + + if (borderOnly && borderThickness > 0f) + { + DrawRoundedBorderGlow(drawList, center, halfSize, borderThickness, edgeColor, peakColor, rounding); + return; + } + + if (!borderOnly || borderThickness <= 0f) + { + const float combinedScale = 0.85f; + DrawHorizontalHighlightRect( + drawList, + highlightLeft, + highlightRight, + highlightTop, + highlightBottom, + edgeColor, + peakColor, + 0.5f, + combinedScale); + + DrawVerticalHighlightRect( + drawList, + highlightLeft, + highlightRight, + highlightTop, + highlightBottom, + edgeColor, + peakColor, + 0.5f, + combinedScale); + return; + } + + var outerLeft = MathF.Max(gradientLeft, highlightLeft - borderThickness); + var outerRight = MathF.Min(gradientRight, highlightRight + borderThickness); + var outerTop = MathF.Max(clampedTopY, highlightTop - borderThickness); + var outerBottom = MathF.Min(clampedBottomY, highlightBottom + borderThickness); + + var edge = ImGui.ColorConvertFloat4ToU32(edgeColor); + var peak = ImGui.ColorConvertFloat4ToU32(peakColor); + + if (outerTop < highlightTop) + { + drawList.AddRectFilledMultiColor( + new Vector2(outerLeft, outerTop), + new Vector2(outerRight, highlightTop), + edge, + edge, + peak, + peak); + } + + if (outerBottom > highlightBottom) + { + drawList.AddRectFilledMultiColor( + new Vector2(outerLeft, highlightBottom), + new Vector2(outerRight, outerBottom), + peak, + peak, + edge, + edge); + } + + if (outerLeft < highlightLeft) + { + drawList.AddRectFilledMultiColor( + new Vector2(outerLeft, highlightTop), + new Vector2(highlightLeft, highlightBottom), + edge, + peak, + peak, + edge); + } + + if (outerRight > highlightRight) + { + drawList.AddRectFilledMultiColor( + new Vector2(highlightRight, highlightTop), + new Vector2(outerRight, highlightBottom), + peak, + edge, + edge, + peak); + } + } + + private static void DrawPointHighlight( + ImDrawListPtr drawList, + Vector2 center, + Vector2 halfSize, + Vector4 edgeColor, + Vector4 peakColor, + bool borderOnly, + float borderThickness) + { + if (halfSize.X <= 0f || halfSize.Y <= 0f) + return; + + if (borderOnly && borderThickness > 0f) + { + DrawPointBorderGlow(drawList, center, halfSize, borderThickness, edgeColor, peakColor); + return; + } + + const int layers = 7; + for (int layer = 0; layer < layers; layer++) + { + float t = layers <= 1 ? 1f : layer / (layers - 1f); + float scale = 1f - 0.75f * t; + var scaledHalfSize = new Vector2(MathF.Max(1f, halfSize.X * scale), MathF.Max(1f, halfSize.Y * scale)); + var color = Vector4.Lerp(edgeColor, peakColor, t); + DrawEllipseFilled(drawList, center, scaledHalfSize, ImGui.ColorConvertFloat4ToU32(color)); + } + } + + private static void DrawPointBorderGlow( + ImDrawListPtr drawList, + Vector2 center, + Vector2 halfSize, + float thickness, + Vector4 edgeColor, + Vector4 peakColor) + { + int layers = Math.Max(6, (int)MathF.Ceiling(thickness)); + for (int i = 0; i < layers; i++) + { + float t = layers <= 1 ? 1f : i / (layers - 1f); + float offset = thickness * t; + var expandedHalfSize = new Vector2(MathF.Max(1f, halfSize.X + offset), MathF.Max(1f, halfSize.Y + offset)); + var color = Vector4.Lerp(peakColor, edgeColor, t); + color.W = Math.Clamp((peakColor.W * 0.8f) + (edgeColor.W - peakColor.W) * t, 0f, 1f); + DrawEllipseStroke(drawList, center, expandedHalfSize, ImGui.ColorConvertFloat4ToU32(color), 2f); + } + } + + private static void DrawEllipseFilled(ImDrawListPtr drawList, Vector2 center, Vector2 halfSize, uint color, int segments = 48) + { + if (halfSize.X <= 0f || halfSize.Y <= 0f) + return; + + BuildEllipsePath(drawList, center, halfSize, segments); + drawList.PathFillConvex(color); + } + + private static void DrawEllipseStroke(ImDrawListPtr drawList, Vector2 center, Vector2 halfSize, uint color, float thickness, int segments = 48) + { + if (halfSize.X <= 0f || halfSize.Y <= 0f) + return; + + BuildEllipsePath(drawList, center, halfSize, segments); + drawList.PathStroke(color, ImDrawFlags.None, MathF.Max(1f, thickness)); + } + + private static void BuildEllipsePath(ImDrawListPtr drawList, Vector2 center, Vector2 halfSize, int segments) + { + const float twoPi = MathF.PI * 2f; + segments = Math.Clamp(segments, 12, 96); + drawList.PathClear(); + for (int i = 0; i < segments; i++) + { + float angle = twoPi * (i / (float)segments); + var point = new Vector2( + center.X + MathF.Cos(angle) * halfSize.X, + center.Y + MathF.Sin(angle) * halfSize.Y); + drawList.PathLineTo(point); + } + } + + private static void DrawRoundedBorderGlow( + ImDrawListPtr drawList, + Vector2 center, + Vector2 halfSize, + float thickness, + Vector4 edgeColor, + Vector4 peakColor, + float rounding) + { + int layers = Math.Max(6, (int)MathF.Ceiling(thickness)); + for (int i = 0; i < layers; i++) + { + float t = layers <= 1 ? 0f : i / (layers - 1f); + float offset = thickness * t; + var min = new Vector2(center.X - halfSize.X - offset, center.Y - halfSize.Y - offset); + var max = new Vector2(center.X + halfSize.X + offset, center.Y + halfSize.Y + offset); + var color = Vector4.Lerp(peakColor, edgeColor, t); + color.W = Math.Clamp((peakColor.W * 0.8f) + (edgeColor.W - peakColor.W) * t, 0f, 1f); + drawList.AddRect(min, max, ImGui.ColorConvertFloat4ToU32(color), MathF.Max(0f, rounding + offset), ImDrawFlags.RoundCornersAll, 2f); + } + } + + private static void DrawHorizontalHighlightRect( + ImDrawListPtr drawList, + float left, + float right, + float top, + float bottom, + Vector4 edgeColor, + Vector4 peakColor, + float midpoint, + float alphaScale) + { + if (right <= left || bottom <= top) + return; + + edgeColor.W *= alphaScale; + peakColor.W *= alphaScale; + + var edge = ImGui.ColorConvertFloat4ToU32(edgeColor); + var peak = ImGui.ColorConvertFloat4ToU32(peakColor); + var highlightMid = top + (bottom - top) * midpoint; + + drawList.AddRectFilledMultiColor( + new Vector2(left, top), + new Vector2(right, highlightMid), + edge, + edge, + peak, + peak); + + drawList.AddRectFilledMultiColor( + new Vector2(left, highlightMid), + new Vector2(right, bottom), + peak, + peak, + edge, + edge); + } + + private static void DrawVerticalHighlightRect( + ImDrawListPtr drawList, + float left, + float right, + float top, + float bottom, + Vector4 edgeColor, + Vector4 peakColor, + float midpoint, + float alphaScale) + { + if (right <= left || bottom <= top) + return; + + edgeColor.W *= alphaScale; + peakColor.W *= alphaScale; + + var edge = ImGui.ColorConvertFloat4ToU32(edgeColor); + var peak = ImGui.ColorConvertFloat4ToU32(peakColor); + var highlightMid = left + (right - left) * midpoint; + + drawList.AddRectFilledMultiColor( + new Vector2(left, top), + new Vector2(highlightMid, bottom), + edge, + peak, + peak, + edge); + + drawList.AddRectFilledMultiColor( + new Vector2(highlightMid, top), + new Vector2(right, bottom), + peak, + edge, + edge, + peak); + } +} diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index be8e1d4..730d124 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -1,25 +1,22 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; -using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; -using LightlessSync.API.Dto.User; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; -using LightlessSync.UI.Handlers; +using LightlessSync.Services.Profiles; +using LightlessSync.UI.Services; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; +using SharpDX.DirectWrite; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; using System.Globalization; -using System.Linq; using System.Numerics; namespace LightlessSync.UI; @@ -30,38 +27,35 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase private readonly bool _isModerator = false; private readonly bool _isOwner = false; private readonly List _oneTimeInvites = []; - private readonly PairManager _pairManager; private readonly LightlessProfileManager _lightlessProfileManager; - private readonly FileDialogManager _fileDialogManager; private readonly UiSharedService _uiSharedService; + private readonly PairUiService _pairUiService; private List _bannedUsers = []; private LightlessGroupProfileData? _profileData = null; - private bool _adjustedForScollBarsLocalProfile = false; - private bool _adjustedForScollBarsOnlineProfile = false; - private string _descriptionText = string.Empty; + private string _userSearchFilter = string.Empty; private IDalamudTextureWrap? _pfpTextureWrap; private string _profileDescription = string.Empty; - private byte[] _profileImage = []; - private bool _showFileDialogError = false; private int _multiInvites; private string _newPassword; private bool _pwChangeSuccess; private Task? _pruneTestTask; private Task? _pruneTask; private int _pruneDays = 14; - private List _selectedTags = []; + + private Task? _pruneSettingsTask; + private bool _pruneSettingsLoaded; + private bool _autoPruneEnabled; + private int _autoPruneDays = 14; public SyncshellAdminUI(ILogger logger, LightlessMediator mediator, ApiController apiController, - UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager, FileDialogManager fileDialogManager) + UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager) : base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService) { GroupFullInfo = groupFullInfo; _apiController = apiController; _uiSharedService = uiSharedService; - _pairManager = pairManager; _lightlessProfileManager = lightlessProfileManager; - _fileDialogManager = fileDialogManager; - + _pairUiService = pairUiService; _isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal); _isModerator = GroupFullInfo.GroupUserInfo.IsModerator(); _newPassword = string.Empty; @@ -81,6 +75,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase MinimumSize = new(700, 500), MaximumSize = new(700, 2000), }; + _pairUiService = pairUiService; } public GroupFullInfoDto GroupFullInfo { get; private set; } @@ -90,30 +85,156 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase if (!_isModerator && !_isOwner) return; _logger.LogTrace("Drawing Syncshell Admin UI for {group}", GroupFullInfo.GroupAliasOrGID); - GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group]; + var snapshot = _pairUiService.GetSnapshot(); + if (snapshot.GroupsByGid.TryGetValue(GroupFullInfo.Group.GID, out var updatedInfo)) + { + GroupFullInfo = updatedInfo; + } _profileData = _lightlessProfileManager.GetLightlessGroupProfile(GroupFullInfo.Group); - GetTagsFromProfile(); - using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID); - using (_uiSharedService.UidFont.Push()) - _uiSharedService.UnderlinedBigText(GroupFullInfo.GroupAliasOrGID + " Administrative Panel", UIColors.Get("LightlessBlue")); + + DrawAdminHeader(); ImGui.Separator(); var perm = GroupFullInfo.GroupPermissions; - using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID); - - if (tabbar) + DrawAdminTopBar(perm); + } + + private void DrawAdminHeader() + { + float scale = ImGuiHelpers.GlobalScale; + var style = ImGui.GetStyle(); + + var cursorLocal = ImGui.GetCursorPos(); + var pMin = ImGui.GetCursorScreenPos(); + float width = ImGui.GetContentRegionAvail().X; + float height = 64f * scale; + + var pMax = new Vector2(pMin.X + width, pMin.Y + height); + var drawList = ImGui.GetWindowDrawList(); + + var purple = UIColors.Get("LightlessPurple"); + var gradLeft = purple.WithAlpha(0.0f); + var gradRight = purple.WithAlpha(0.85f); + + uint colTopLeft = ImGui.ColorConvertFloat4ToU32(gradLeft); + uint colTopRight = ImGui.ColorConvertFloat4ToU32(gradRight); + uint colBottomRight = ImGui.ColorConvertFloat4ToU32(gradRight); + uint colBottomLeft = ImGui.ColorConvertFloat4ToU32(gradLeft); + + drawList.AddRectFilledMultiColor( + pMin, + pMax, + colTopLeft, + colTopRight, + colBottomRight, + colBottomLeft); + + float accentHeight = 3f * scale; + var accentMin = new Vector2(pMin.X, pMax.Y - accentHeight); + var accentMax = new Vector2(pMax.X, pMax.Y); + var accentColor = UIColors.Get("LightlessBlue"); + uint accentU32 = ImGui.ColorConvertFloat4ToU32(accentColor); + drawList.AddRectFilled(accentMin, accentMax, accentU32); + + ImGui.InvisibleButton("##adminHeaderHitbox", new Vector2(width, height)); + + if (ImGui.IsItemHovered()) { - DrawInvites(perm); - - DrawManagement(); - - DrawPermission(perm); - - DrawProfile(); + ImGui.BeginTooltip(); + ImGui.Text($"{GroupFullInfo.GroupAliasOrGID} is created at:"); + ImGui.Separator(); + ImGui.Text(GroupFullInfo.Group.CreatedAt?.ToString("yyyy-MM-dd HH:mm:ss 'UTC'") ?? "Unknown"); + ImGui.EndTooltip(); } + + var titlePos = new Vector2(pMin.X + 12f * scale, pMin.Y + 8f * scale); + ImGui.SetCursorScreenPos(titlePos); + + float titleHeight; + using (_uiSharedService.UidFont.Push()) + { + ImGui.TextColored(UIColors.Get("LightlessBlue"), GroupFullInfo.GroupAliasOrGID); + titleHeight = ImGui.GetTextLineHeightWithSpacing(); + } + + var subtitlePos = new Vector2( + pMin.X + 12f * scale, + titlePos.Y + titleHeight - 2f * scale); + + ImGui.SetCursorScreenPos(subtitlePos); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); + ImGui.TextUnformatted("Administrative Panel"); + ImGui.PopStyleColor(); + + string roleLabel = _isOwner ? "Owner" : (_isModerator ? "Moderator" : string.Empty); + if (!string.IsNullOrEmpty(roleLabel)) + { + float roleTextW = ImGui.CalcTextSize(roleLabel).X; + float pillPaddingX = 8f * scale; + float pillPaddingY = -1f * scale; + + float pillWidth = roleTextW + pillPaddingX * 2f; + float pillHeight = ImGui.GetTextLineHeight() + pillPaddingY * 2f; + + var pillMin = new Vector2( + pMax.X - pillWidth - style.WindowPadding.X, + subtitlePos.Y - pillPaddingY); + var pillMax = new Vector2(pillMin.X + pillWidth, pillMin.Y + pillHeight); + + var pillBg = _isOwner ? UIColors.Get("LightlessYellow") : UIColors.Get("LightlessOrange"); + uint pillBgU = ImGui.ColorConvertFloat4ToU32(pillBg.WithAlpha(0.9f)); + + drawList.AddRectFilled(pillMin, pillMax, pillBgU, 8f * scale); + + ImGui.SetCursorScreenPos(new Vector2(pillMin.X + pillPaddingX, pillMin.Y + pillPaddingY)); + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("FullBlack")); + ImGui.TextUnformatted(roleLabel); + ImGui.PopStyleColor(); + } + + ImGui.SetCursorPos(new Vector2(cursorLocal.X, cursorLocal.Y + height + 6f * scale)); + } + + private void DrawAdminTopBar(GroupPermissions perm) + { + var style = ImGui.GetStyle(); + + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(12f, 6f) * ImGuiHelpers.GlobalScale); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(10f, style.ItemSpacing.Y)); + ImGui.PushStyleVar(ImGuiStyleVar.TabRounding, 6f * ImGuiHelpers.GlobalScale); + + var baseTab = UIColors.Get("FullBlack").WithAlpha(0.0f); + var baseTabDim = UIColors.Get("FullBlack").WithAlpha(0.1f); + var accent = UIColors.Get("LightlessPurple"); + var accentHover = accent.WithAlpha(0.90f); + var accentActive = accent; + + using (ImRaii.PushColor(ImGuiCol.Tab, baseTab)) + using (ImRaii.PushColor(ImGuiCol.TabHovered, accentHover)) + using (ImRaii.PushColor(ImGuiCol.TabActive, accentActive)) + using (ImRaii.PushColor(ImGuiCol.TabUnfocused, baseTabDim)) + using (ImRaii.PushColor(ImGuiCol.TabUnfocusedActive, accentActive.WithAlpha(0.80f))) + { + using (var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID)) + { + if (tabbar) + { + DrawInvites(perm); + DrawManagement(); + DrawPermission(perm); + DrawProfile(); + } + } + } + + ImGui.PopStyleVar(3); + + ImGuiHelpers.ScaledDummy(2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGuiHelpers.ScaledDummy(2f); } private void DrawPermission(GroupPermissions perm) @@ -212,482 +333,332 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase ownerTab.Dispose(); } } + + private void DrawAutoPruneSettings() + { + ImGuiHelpers.ScaledDummy(2f); + UiSharedService.TextWrapped("Automatic prune (server-side scheduled cleanup of inactive users)."); + + _pruneSettingsTask ??= _apiController.GroupGetPruneSettings(new GroupDto(GroupFullInfo.Group)); + + if (!_pruneSettingsLoaded) + { + if (!_pruneSettingsTask!.IsCompleted) + { + UiSharedService.ColorTextWrapped("Loading prune settings from server...", ImGuiColors.DalamudGrey); + return; + } + + if (_pruneSettingsTask.IsFaulted || _pruneSettingsTask.IsCanceled) + { + UiSharedService.ColorTextWrapped("Failed to load auto-prune settings.", ImGuiColors.DalamudRed); + _pruneSettingsTask = null; + _pruneSettingsLoaded = false; + return; + } + + var dto = _pruneSettingsTask.GetAwaiter().GetResult(); + + _autoPruneEnabled = dto.AutoPruneEnabled && dto.AutoPruneDays > 0; + _autoPruneDays = dto.AutoPruneDays > 0 ? dto.AutoPruneDays : 14; + + _pruneSettingsLoaded = true; + } + + bool enabled = _autoPruneEnabled; + if (ImGui.Checkbox("Enable automatic pruning", ref enabled)) + { + _autoPruneEnabled = enabled; + SavePruneSettings(); + } + UiSharedService.AttachToolTip("When enabled, inactive non-pinned, non-moderator users will be pruned automatically on the server."); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(150); + + using (ImRaii.Disabled(!_autoPruneEnabled)) + { + _uiSharedService.DrawCombo( + "Day(s) of inactivity", + [1, 3, 7, 14, 30, 90], + days => $"{days} day(s)", + selected => + { + _autoPruneDays = selected; + SavePruneSettings(); + }, + _autoPruneDays); + } + + if (!_autoPruneEnabled) + { + UiSharedService.ColorTextWrapped( + "Automatic prune is currently disabled. Enable it and choose an inactivity threshold to let the server clean up inactive users automatically.", + ImGuiColors.DalamudGrey); + } + } + private void DrawProfile() { var profileTab = ImRaii.TabItem("Profile"); + if (!profileTab) + return; - if (profileTab) + if (_profileData != null) { - if (_uiSharedService.MediumTreeNode("Current Profile", UIColors.Get("LightlessPurple"))) + if (!string.Equals(_profileDescription, _profileData.Description, StringComparison.Ordinal)) { - ImGui.Dummy(new Vector2(5)); - - if (!_profileImage.SequenceEqual(_profileData.ImageData.Value)) - { - _profileImage = _profileData.ImageData.Value; - _pfpTextureWrap?.Dispose(); - _pfpTextureWrap = _uiSharedService.LoadImage(_profileImage); - } - - if (!string.Equals(_profileDescription, _profileData.Description, StringComparison.OrdinalIgnoreCase)) - { - _profileDescription = _profileData.Description; - _descriptionText = _profileDescription; - } - - if (_pfpTextureWrap != null) - { - ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height)); - } - - var spacing = ImGui.GetStyle().ItemSpacing.X; - ImGuiHelpers.ScaledRelativeSameLine(256, spacing); - using (_uiSharedService.GameFont.Push()) - { - var descriptionTextSize = ImGui.CalcTextSize(_profileData.Description, wrapWidth: 256f); - var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256); - if (descriptionTextSize.Y > childFrame.Y) - { - _adjustedForScollBarsOnlineProfile = true; - } - else - { - _adjustedForScollBarsOnlineProfile = false; - } - childFrame = childFrame with - { - X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0), - }; - if (ImGui.BeginChildFrame(101, childFrame)) - { - UiSharedService.TextWrapped(_profileData.Description); - } - ImGui.EndChildFrame(); - ImGui.TreePop(); - } - var nsfw = _profileData.IsNsfw; - ImGui.BeginDisabled(); - ImGui.Checkbox("Is NSFW", ref nsfw); - ImGui.EndDisabled(); + _profileDescription = _profileData.Description; } - ImGui.Separator(); + UiSharedService.TextWrapped("Preview the Syncshell profile in a standalone window."); - if (_uiSharedService.MediumTreeNode("Profile Settings", UIColors.Get("LightlessPurple"))) + if (_uiSharedService.IconTextButton(FontAwesomeIcon.AddressCard, "Open Syncshell Profile")) { - ImGui.Dummy(new Vector2(5)); - ImGui.TextUnformatted($"Profile Picture:"); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) - { - _fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) => - { - if (!success) return; - _ = Task.Run(async () => - { - var fileContent = await File.ReadAllBytesAsync(file).ConfigureAwait(false); - MemoryStream ms = new(fileContent); - await using (ms.ConfigureAwait(false)) - { - var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); - if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) - { - _showFileDialogError = true; - return; - } - using var image = Image.Load(fileContent); - - if (image.Width > 512 || image.Height > 512 || (fileContent.Length > 2000 * 1024)) - { - _showFileDialogError = true; - return; - } - - _showFileDialogError = false; - await _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, Convert.ToBase64String(fileContent), BannerBase64: null, IsNsfw: null, IsDisabled: null)) - .ConfigureAwait(false); - } - }); - }); - } - UiSharedService.AttachToolTip("Select and upload a new profile picture"); - ImGui.SameLine(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture")) - { - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - UiSharedService.AttachToolTip("Clear your currently uploaded profile picture"); - if (_showFileDialogError) - { - UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed); - } - ImGui.Separator(); - ImGui.TextUnformatted($"Tags:"); - var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200); - - var allCategoryIndexes = Enum.GetValues() - .Cast() - .ToList(); - - foreach(int tag in allCategoryIndexes) - { - using (ImRaii.PushId($"tag-{tag}")) DrawTag(tag); - } - ImGui.Separator(); - var widthTextBox = 400; - var posX = ImGui.GetCursorPosX(); - ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500"); - ImGui.SetCursorPosX(posX); - ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X); - ImGui.TextUnformatted("Preview (approximate)"); - using (_uiSharedService.GameFont.Push()) - ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200)); - - ImGui.SameLine(); - - using (_uiSharedService.GameFont.Push()) - { - var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f); - if (descriptionTextSizeLocal.Y > childFrameLocal.Y) - { - _adjustedForScollBarsLocalProfile = true; - } - else - { - _adjustedForScollBarsLocalProfile = false; - } - childFrameLocal = childFrameLocal with - { - X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0), - }; - if (ImGui.BeginChildFrame(102, childFrameLocal)) - { - UiSharedService.TextWrapped(_descriptionText); - } - ImGui.EndChildFrame(); - } - - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description")) - { - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: _descriptionText, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - UiSharedService.AttachToolTip("Sets your profile description text"); - ImGui.SameLine(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description")) - { - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - UiSharedService.AttachToolTip("Clears your profile description text"); - ImGui.Separator(); - ImGui.TextUnformatted($"Profile Options:"); - var isNsfw = _profileData.IsNsfw; - if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) - { - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: isNsfw, IsDisabled: null)); - } - _uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON"); - ImGui.TreePop(); + Mediator.Publish(new GroupProfileOpenStandaloneMessage(GroupFullInfo.Group)); } + UiSharedService.AttachToolTip("Opens the standalone Syncshell profile window for this group."); + + ImGuiHelpers.ScaledDummy(2f); + ImGui.TextDisabled("Profile Flags"); + ImGui.BulletText(_profileData.IsNsfw ? "Marked as NSFW" : "Marked as SFW"); + ImGui.BulletText(_profileData.IsDisabled ? "Profile disabled for viewers" : "Profile active"); + + ImGuiHelpers.ScaledDummy(2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGuiHelpers.ScaledDummy(2f); + + UiSharedService.TextWrapped("Open the syncshell profile editor to update images, description, tags, and visibility settings."); + ImGuiHelpers.ScaledDummy(2f); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserEdit, "Open Syncshell Profile Editor")) + { + Mediator.Publish(new OpenGroupProfileEditorMessage(GroupFullInfo)); + } + UiSharedService.AttachToolTip("Launches the editor window and associated live preview for this syncshell."); } + else + { + UiSharedService.TextWrapped("Profile information is loading..."); + } + profileTab.Dispose(); } private void DrawManagement() { var mgmtTab = ImRaii.TabItem("User Management"); - if (mgmtTab) + if (!mgmtTab) + return; + + ImGuiHelpers.ScaledDummy(3f); + + var style = ImGui.GetStyle(); + + var baseTab = UIColors.Get("FullBlack").WithAlpha(0.0f); + var baseTabDim = UIColors.Get("FullBlack").WithAlpha(0.1f); + var accent = UIColors.Get("LightlessPurple"); + var accentHover = accent.WithAlpha(0.90f); + var accentActive = accent; + + //Pushing style vars for inner tab bar + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(10f, 5f) * ImGuiHelpers.GlobalScale); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(8f, style.ItemSpacing.Y)); + ImGui.PushStyleVar(ImGuiStyleVar.TabRounding, 5f * ImGuiHelpers.GlobalScale); + + try { - if (_uiSharedService.MediumTreeNode("User List & Administration", UIColors.Get("LightlessPurple"))) + //Pushing color stack for inner tab bar + using (ImRaii.PushColor(ImGuiCol.Tab, baseTab)) + using (ImRaii.PushColor(ImGuiCol.TabHovered, accentHover)) + using (ImRaii.PushColor(ImGuiCol.TabActive, accentActive)) + using (ImRaii.PushColor(ImGuiCol.TabUnfocused, baseTabDim)) + using (ImRaii.PushColor(ImGuiCol.TabUnfocusedActive, accentActive.WithAlpha(0.80f))) { - if (!_pairManager.GroupPairs.TryGetValue(GroupFullInfo, out var pairs)) + using (var innerTabBar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID)) { - UiSharedService.ColorTextWrapped("No users found in this Syncshell", ImGuiColors.DalamudYellow); - } - else - { - var tableFlags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp; - if (pairs.Count > 10) tableFlags |= ImGuiTableFlags.ScrollY; - using var table = ImRaii.Table("userList#" + GroupFullInfo.Group.AliasOrGID, 3, tableFlags); - if (table) + if (innerTabBar) { - ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 4); - ImGui.TableSetupColumn("Flags", ImGuiTableColumnFlags.None, 1); - ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 3); - ImGui.TableHeadersRow(); - - var groupedPairs = new Dictionary(pairs.Select(p => new KeyValuePair(p, - GroupFullInfo.GroupPairUserInfos.TryGetValue(p.UserData.UID, out GroupPairUserInfo value) ? value : null))); - - foreach (var pair in groupedPairs.OrderBy(p => + // Users tab + var usersTab = ImRaii.TabItem("Users"); + if (usersTab) { - if (p.Value == null) return 10; - if (string.Equals(p.Key.UserData.UID, GroupFullInfo.OwnerUID, StringComparison.Ordinal)) return 0; - if (p.Value.Value.IsModerator()) return 1; - if (p.Value.Value.IsPinned()) return 2; - return 10; - }).ThenBy(p => p.Key.GetNote() ?? p.Key.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase)) - { - using var tableId = ImRaii.PushId("userTable_" + pair.Key.UserData.UID); - var isUserOwner = string.Equals(pair.Key.UserData.UID, GroupFullInfo.OwnerUID, StringComparison.Ordinal); - - ImGui.TableNextColumn(); // alias/uid/note - var note = pair.Key.GetNote(); - var text = note == null ? pair.Key.UserData.AliasOrUID : note + " (" + pair.Key.UserData.AliasOrUID + ")"; - ImGui.AlignTextToFramePadding(); - var boolcolor = UiSharedService.GetBoolColor(pair.Key.IsOnline); - UiSharedService.ColorText(text, boolcolor); - if (!string.IsNullOrEmpty(pair.Key.PlayerName)) - { - UiSharedService.AttachToolTip(pair.Key.PlayerName); - ImGui.SameLine(); - } - - ImGui.TableNextColumn(); // special flags - if (pair.Value != null && (pair.Value.Value.IsModerator() || pair.Value.Value.IsPinned() || isUserOwner)) - { - if (pair.Value.Value.IsModerator()) - { - _uiSharedService.IconText(FontAwesomeIcon.UserShield, UIColors.Get("LightlessPurple")); - UiSharedService.AttachToolTip("Moderator"); - } - if (pair.Value.Value.IsPinned() && !isUserOwner) - { - _uiSharedService.IconText(FontAwesomeIcon.Thumbtack); - UiSharedService.AttachToolTip("Pinned"); - } - if (isUserOwner) - { - _uiSharedService.IconText(FontAwesomeIcon.Crown, UIColors.Get("LightlessYellow")); - UiSharedService.AttachToolTip("Owner"); - } - } - else - { - _uiSharedService.IconText(FontAwesomeIcon.None); - } - - ImGui.TableNextColumn(); // actions - if (_isOwner) - { - using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"))) - { - using (ImRaii.Disabled(!UiSharedService.ShiftPressed())) - { - if (_uiSharedService.IconButton(FontAwesomeIcon.Crown)) - { - _ = _apiController.GroupChangeOwnership(new(GroupFullInfo.Group, pair.Key.UserData)); - IsOpen = false; - } - } - - } - UiSharedService.AttachToolTip("Hold SHIFT and click to transfer ownership of this Syncshell to " - + (pair.Key.UserData.AliasOrUID) + Environment.NewLine + "WARNING: This action is irreversible and will close screen."); - ImGui.SameLine(); - - using (ImRaii.PushColor(ImGuiCol.Text, pair.Value != null && pair.Value.Value.IsModerator() ? UIColors.Get("DimRed") : UIColors.Get("PairBlue"))) - { - if (_uiSharedService.IconButton(FontAwesomeIcon.UserShield)) - { - GroupPairUserInfo userInfo = pair.Value ?? GroupPairUserInfo.None; - - userInfo.SetModerator(!userInfo.IsModerator()); - - _ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(GroupFullInfo.Group, pair.Key.UserData, userInfo)); - } - } - UiSharedService.AttachToolTip(pair.Value != null && pair.Value.Value.IsModerator() ? "Demod user" : "Mod user"); - ImGui.SameLine(); - } - - if (pair.Value == null || pair.Value != null && !pair.Value.Value.IsModerator() && !isUserOwner) - { - using (ImRaii.PushColor(ImGuiCol.Text, pair.Value != null && pair.Value.Value.IsPinned() ? UIColors.Get("DimRed") : UIColors.Get("PairBlue"))) - { - if (_uiSharedService.IconButton(FontAwesomeIcon.Thumbtack)) - { - GroupPairUserInfo userInfo = pair.Value ?? GroupPairUserInfo.None; - - userInfo.SetPinned(!userInfo.IsPinned()); - - _ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(GroupFullInfo.Group, pair.Key.UserData, userInfo)); - } - } - UiSharedService.AttachToolTip(pair.Value != null && pair.Value.Value.IsPinned() ? "Unpin user" : "Pin user"); - ImGui.SameLine(); - - using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed"))) - { - using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) - { - if (_uiSharedService.IconButton(FontAwesomeIcon.Trash)) - { - _ = _apiController.GroupRemoveUser(new GroupPairDto(GroupFullInfo.Group, pair.Key.UserData)); - } - } - } - UiSharedService.AttachToolTip("Remove user from Syncshell" - + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); - ImGui.SameLine(); - - using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed"))) - { - using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) - { - if (_uiSharedService.IconButton(FontAwesomeIcon.Ban)) - { - Mediator.Publish(new OpenBanUserPopupMessage(pair.Key, GroupFullInfo)); - } - } - } - UiSharedService.AttachToolTip("Ban user from Syncshell" - + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); - } + DrawUserListSection(); } - } - } - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); - } - ImGui.Separator(); + usersTab.Dispose(); - if (_uiSharedService.MediumTreeNode("Mass Cleanup", UIColors.Get("DimRed"))) - { - using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) - { - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Clear Syncshell")) - { - _ = _apiController.GroupClear(new(GroupFullInfo.Group)); - } - } - UiSharedService.AttachToolTip("This will remove all non-pinned, non-moderator users from the Syncshell." - + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); - - ImGui.SameLine(); - - using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) - { - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Brush, "Clear Lightfinder Users")) - { - _ = _apiController.GroupClearFinder(new(GroupFullInfo.Group)); - } - } - UiSharedService.AttachToolTip("This will remove all users that joined through Lightfinder from the Syncshell." - + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); - - ImGuiHelpers.ScaledDummy(2f); - ImGui.Separator(); - ImGuiHelpers.ScaledDummy(2f); - - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Unlink, "Check for Inactive Users")) - { - _pruneTestTask = _apiController.GroupPrune(new(GroupFullInfo.Group), _pruneDays, execute: false); - _pruneTask = null; - } - UiSharedService.AttachToolTip($"This will start the prune process for this Syncshell of inactive Lightless users that have not logged in in the past {_pruneDays} day(s)." - + Environment.NewLine + "You will be able to review the amount of inactive users before executing the prune." - + UiSharedService.TooltipSeparator + "Note: this check excludes pinned users and moderators of this Syncshell."); - ImGui.SameLine(); - ImGui.SetNextItemWidth(150); - _uiSharedService.DrawCombo("Day(s) of inactivity", [1, 3, 7, 14, 30, 90], (count) => - { - return count + " day(s)"; - }, - (selected) => - { - _pruneDays = selected; - _pruneTestTask = null; - _pruneTask = null; - }, - _pruneDays); - - if (_pruneTestTask != null) - { - if (!_pruneTestTask.IsCompleted) - { - UiSharedService.ColorTextWrapped("Calculating inactive users...", ImGuiColors.DalamudYellow); - } - else - { - ImGui.AlignTextToFramePadding(); - UiSharedService.TextWrapped($"Found {_pruneTestTask.Result} user(s) that have not logged into Lightless in the past {_pruneDays} day(s)."); - if (_pruneTestTask.Result > 0) + // Cleanup tab + var cleanupTab = ImRaii.TabItem("Cleanup"); + if (cleanupTab) { - using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) - { - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Prune Inactive Users")) - { - _pruneTask = _apiController.GroupPrune(new(GroupFullInfo.Group), _pruneDays, execute: true); - _pruneTestTask = null; - } - } - UiSharedService.AttachToolTip($"Pruning will remove {_pruneTestTask?.Result ?? 0} inactive user(s)." - + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); + DrawMassCleanupSection(); } - } - } - if (_pruneTask != null) - { - if (!_pruneTask.IsCompleted) - { - UiSharedService.ColorTextWrapped("Pruning Syncshell...", ImGuiColors.DalamudYellow); - } - else - { - UiSharedService.TextWrapped($"Syncshell was pruned and {_pruneTask.Result} inactive user(s) have been removed."); - } - } - _uiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f); - ImGui.TreePop(); - } - ImGui.Separator(); + cleanupTab.Dispose(); - if (_uiSharedService.MediumTreeNode("User Bans", UIColors.Get("LightlessYellow"))) - { - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) - { - _bannedUsers = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group)).Result; - } - var tableFlags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp; - if (_bannedUsers.Count > 10) tableFlags |= ImGuiTableFlags.ScrollY; - if (ImGui.BeginTable("bannedusertable" + GroupFullInfo.GID, 6, tableFlags)) - { - ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, 1); - ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.None, 1); - ImGui.TableSetupColumn("By", ImGuiTableColumnFlags.None, 1); - ImGui.TableSetupColumn("Date", ImGuiTableColumnFlags.None, 2); - ImGui.TableSetupColumn("Reason", ImGuiTableColumnFlags.None, 3); - ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 1); - - ImGui.TableHeadersRow(); - - foreach (var bannedUser in _bannedUsers.ToList()) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(bannedUser.UID); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(bannedUser.UserAlias ?? string.Empty); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(bannedUser.BannedBy); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture)); - ImGui.TableNextColumn(); - UiSharedService.TextWrapped(bannedUser.Reason); - ImGui.TableNextColumn(); - using var _ = ImRaii.PushId(bannedUser.UID); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Check, "Unban")) + // Bans tab + var bansTab = ImRaii.TabItem("Bans"); + if (bansTab) { - _apiController.GroupUnbanUser(bannedUser); - _bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal)); + DrawUserBansSection(); } + bansTab.Dispose(); } - ImGui.EndTable(); } - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); - ImGui.TreePop(); } - ImGui.Separator(); + mgmtTab.Dispose(); } - mgmtTab.Dispose(); + finally + { + // Popping style vars (3) for inner tab bar + ImGui.PopStyleVar(3); + } + } + + private void DrawUserListSection() + { + var snapshot = _pairUiService.GetSnapshot(); + if (!snapshot.GroupPairs.TryGetValue(GroupFullInfo, out var pairs)) + { + UiSharedService.ColorTextWrapped("No users found in this Syncshell", ImGuiColors.DalamudYellow); + return; + } + + _uiSharedService.MediumText("User List & Administration", UIColors.Get("LightlessPurple")); + ImGuiHelpers.ScaledDummy(2f); + DrawUserListCustom(pairs, GroupFullInfo); + } + + private void DrawMassCleanupSection() + { + _uiSharedService.MediumText("Mass Cleanup", UIColors.Get("DimRed")); + UiSharedService.TextWrapped("Tools to bulk-clean inactive or unwanted users from this Syncshell."); + ImGuiHelpers.ScaledDummy(3f); + + using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Clear Syncshell")) + { + _ = _apiController.GroupClear(new(GroupFullInfo.Group)); + } + } + UiSharedService.AttachToolTip("This will remove all non-pinned, non-moderator users from the Syncshell." + + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); + + ImGui.SameLine(); + + using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Brush, "Clear Lightfinder Users")) + { + _ = _apiController.GroupClearFinder(new(GroupFullInfo.Group)); + } + } + UiSharedService.AttachToolTip("This will remove all users that joined through Lightfinder from the Syncshell." + + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); + + ImGuiHelpers.ScaledDummy(2f); + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.0f); + ImGuiHelpers.ScaledDummy(2f); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Unlink, "Check for Inactive Users")) + { + _pruneTestTask = _apiController.GroupPrune(new(GroupFullInfo.Group), _pruneDays, execute: false); + _pruneTask = null; + } + UiSharedService.AttachToolTip($"This will start the prune process for this Syncshell of inactive Lightless users that have not logged in in the past {_pruneDays} day(s)." + + Environment.NewLine + "You will be able to review the amount of inactive users before executing the prune." + + UiSharedService.TooltipSeparator + "Note: this check excludes pinned users and moderators of this Syncshell."); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(150); + _uiSharedService.DrawCombo( + "Day(s) of inactivity", + [0, 1, 3, 7, 14, 30, 90], + (count) => count == 0 ? "15 minute(s)" : count + " day(s)", + (selected) => + { + _pruneDays = selected; + _pruneTestTask = null; + _pruneTask = null; + }, + _pruneDays); + + if (_pruneTestTask != null) + { + if (!_pruneTestTask.IsCompleted) + { + UiSharedService.ColorTextWrapped("Calculating inactive users...", ImGuiColors.DalamudYellow); + } + else + { + ImGui.AlignTextToFramePadding(); + UiSharedService.TextWrapped($"Found {_pruneTestTask.Result} user(s) that have not logged into Lightless in the past {_pruneDays} day(s)."); + if (_pruneTestTask.Result > 0) + { + using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Broom, "Prune Inactive Users")) + { + _pruneTask = _apiController.GroupPrune(new(GroupFullInfo.Group), _pruneDays, execute: true); + _pruneTestTask = null; + } + } + UiSharedService.AttachToolTip($"Pruning will remove {_pruneTestTask?.Result ?? 0} inactive user(s)." + + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); + } + } + } + + if (_pruneTask != null) + { + if (!_pruneTask.IsCompleted) + { + UiSharedService.ColorTextWrapped("Pruning Syncshell...", ImGuiColors.DalamudYellow); + } + else + { + UiSharedService.TextWrapped($"Syncshell was pruned and {_pruneTask.Result} inactive user(s) have been removed."); + } + } + + ImGuiHelpers.ScaledDummy(4f); + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.0f); + ImGuiHelpers.ScaledDummy(2f); + + DrawAutoPruneSettings(); + } + + private void DrawUserBansSection() + { + _uiSharedService.MediumText("User Bans", UIColors.Get("LightlessYellow")); + ImGuiHelpers.ScaledDummy(3f); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) + { + _bannedUsers = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group)).Result; + } + ImGuiHelpers.ScaledDummy(2f); + + ImGui.BeginChild("bannedListScroll#" + GroupFullInfo.GID, new Vector2(0, 0), true); + + var style = ImGui.GetStyle(); + float fullW = ImGui.GetContentRegionAvail().X; + + float colIdentity = fullW * 0.45f; + float colMeta = fullW * 0.35f; + float colActions = fullW - colIdentity - colMeta - style.ItemSpacing.X * 2.0f; + + // Header + DrawBannedListHeader(colIdentity, colMeta); + + int rowIndex = 0; + foreach (var bannedUser in _bannedUsers.ToList()) + { + // Each row + DrawBannedRow(bannedUser, rowIndex++, colIdentity, colMeta, colActions); + } + + ImGui.EndChild(); } private void DrawInvites(GroupPermissions perm) @@ -734,32 +705,405 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } inviteTab.Dispose(); } - private void DrawTag(int tag) - { - var HasTag = _selectedTags.Contains(tag); - var tagName = (ProfileTags)tag; - if (ImGui.Checkbox(tagName.ToString(), ref HasTag)) + private void DrawUserListCustom(IReadOnlyList pairs, GroupFullInfoDto GroupFullInfo) + { + // Search bar + ImGui.PushItemWidth(0); + _uiSharedService.IconText(FontAwesomeIcon.Search, UIColors.Get("LightlessPurple")); + ImGui.SameLine(); + + ImGui.InputTextWithHint( + "##UserSearchFilter", + "Search UID/alias or note...", + ref _userSearchFilter, + 64); + + ImGui.PopItemWidth(); + ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); + + var groupedPairs = new Dictionary( + pairs.Select(p => new KeyValuePair( + p, + GroupFullInfo.GroupPairUserInfos.TryGetValue(p.UserData.UID, out var value) ? value : null + )) + ); + + var filter = _userSearchFilter?.Trim(); + bool hasFilter = !string.IsNullOrEmpty(filter); + if (hasFilter) + filter = filter!.ToLowerInvariant(); + + var orderedPairs = groupedPairs + .Where(p => !hasFilter || MatchesUserFilter(p.Key, filter!)) + .OrderBy(p => + { + if (p.Value == null) return 10; + if (string.Equals(p.Key.UserData.UID, GroupFullInfo.OwnerUID, StringComparison.Ordinal)) return 0; + if (p.Value.Value.IsModerator()) return 1; + if (p.Value.Value.IsPinned()) return 2; + return 10; + }) + .ThenBy(p => p.Key.GetNote() ?? p.Key.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase) + .ToList(); + + DrawUserListHeader(); + + ImGui.BeginChild("userListScroll#" + GroupFullInfo.Group.AliasOrGID, new Vector2(0, 0), true); + + int rowIndex = 0; + foreach (var kv in orderedPairs) { - if (HasTag) + var pair = kv.Key; + var userInfoOpt = kv.Value; + DrawUserRowCustom(pair, userInfoOpt, GroupFullInfo, rowIndex++); + } + + ImGui.EndChild(); + } + + private static void DrawUserListHeader() + { + var style = ImGui.GetStyle(); + float x0 = ImGui.GetCursorPosX(); + float fullW = ImGui.GetContentRegionAvail().X; + + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessPurple")); + + ImGui.SetCursorPosX(x0); + ImGui.TextUnformatted("User"); + + const string actionsLabel = "Actions"; + float labelWidth = ImGui.CalcTextSize(actionsLabel).X; + + ImGui.SameLine(); + ImGui.SetCursorPosX(x0 + fullW - labelWidth); + ImGui.TextUnformatted(actionsLabel); + + ImGui.PopStyleColor(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.0f); + } + + private void DrawUserRowCustom(Pair pair, GroupPairUserInfo? userInfoOpt, GroupFullInfoDto GroupFullInfo, int rowIndex) + { + using var id = ImRaii.PushId("userRow_" + pair.UserData.UID); + + var style = ImGui.GetStyle(); + Vector2 rowStart = ImGui.GetCursorPos(); + Vector2 rowStartScr = ImGui.GetCursorScreenPos(); + float fullW = ImGui.GetContentRegionAvail().X; + + float frameH = ImGui.GetFrameHeight(); + float textH = ImGui.GetTextLineHeight(); + float rowHeight = frameH; + + if (rowIndex % 2 == 0) + { + var drawList = ImGui.GetWindowDrawList(); + var pMin = rowStartScr; + var pMax = new Vector2(pMin.X + fullW, pMin.Y + rowHeight); + + var bgColor = UIColors.Get("FullBlack").WithAlpha(0.05f); + drawList.AddRectFilled(pMin, pMax, ImGui.ColorConvertFloat4ToU32(bgColor)); + } + + var isUserOwner = string.Equals(pair.UserData.UID, GroupFullInfo.OwnerUID, StringComparison.Ordinal); + var userInfo = userInfoOpt ?? GroupPairUserInfo.None; + + float baselineY = rowStart.Y + (rowHeight - textH) / 2f; + + ImGui.SetCursorPos(new Vector2(rowStart.X, baselineY)); + + bool hasFlag = false; + if (userInfoOpt != null && (userInfo.IsModerator() || userInfo.IsPinned() || isUserOwner)) + { + if (userInfo.IsModerator()) { - _selectedTags.Add(tag); - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: _selectedTags.ToArray(), PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); + _uiSharedService.IconText(FontAwesomeIcon.UserShield, UIColors.Get("LightlessPurple")); + UiSharedService.AttachToolTip("Moderator"); + hasFlag = true; } - else + + if (userInfo.IsPinned() && !isUserOwner) { - _selectedTags.Remove(tag); - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: _selectedTags.ToArray(), PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); + if (hasFlag) ImGui.SameLine(0f, style.ItemSpacing.X); + _uiSharedService.IconText(FontAwesomeIcon.Thumbtack); + UiSharedService.AttachToolTip("Pinned"); + hasFlag = true; } + + if (isUserOwner) + { + if (hasFlag) ImGui.SameLine(0f, style.ItemSpacing.X); + _uiSharedService.IconText(FontAwesomeIcon.Crown, UIColors.Get("LightlessYellow")); + UiSharedService.AttachToolTip("Owner"); + hasFlag = true; + } + } + + if (hasFlag) + ImGui.SameLine(0f, style.ItemSpacing.X); + + ImGui.SetCursorPosY(baselineY); + + var note = pair.GetNote(); + var text = note == null + ? pair.UserData.AliasOrUID + : $"{note} ({pair.UserData.AliasOrUID})"; + + var boolcolor = UiSharedService.GetBoolColor(pair.IsOnline); + UiSharedService.ColorText(text, boolcolor); + + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(pair.UserData.AliasOrUID); + + if (!string.IsNullOrEmpty(pair.PlayerName)) + UiSharedService.AttachToolTip(pair.PlayerName + $"{Environment.NewLine}Click to copy UID or Alias"); + + DrawUserActions(pair, GroupFullInfo, userInfo, isUserOwner, baselineY); + + // Move cursor to next row + ImGui.SetCursorPos(new Vector2(rowStart.X, rowStart.Y + rowHeight + style.ItemSpacing.Y)); + } + + private void DrawUserActions(Pair pair, GroupFullInfoDto GroupFullInfo, GroupPairUserInfo userInfo, bool isUserOwner, float baselineY) + { + var style = ImGui.GetStyle(); + float frameH = ImGui.GetFrameHeight(); + + int buttonCount = 0; + if (_isOwner) + buttonCount += 2; // Crown + Mod + if (userInfo == GroupPairUserInfo.None || (!userInfo.IsModerator() && !isUserOwner)) + buttonCount += 3; // Pin + Trash + Ban + + if (buttonCount == 0) + return; + + float totalWidth = buttonCount * frameH + (buttonCount - 1) * style.ItemSpacing.X; + + float curX = ImGui.GetCursorPosX(); + float avail = ImGui.GetContentRegionAvail().X; + + float startX = curX + MathF.Max(0, avail - (totalWidth + 30f)); + + ImGui.SetCursorPos(new Vector2(startX, baselineY)); + + bool first = true; + + if (_isOwner) + { + // Transfer ownership + using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"))) + using (ImRaii.Disabled(!UiSharedService.ShiftPressed())) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Crown)) + { + _ = _apiController.GroupChangeOwnership(new(GroupFullInfo.Group, pair.UserData)); + IsOpen = false; + } + } + + UiSharedService.AttachToolTip("Hold SHIFT and click to transfer ownership of this Syncshell to " + + pair.UserData.AliasOrUID + Environment.NewLine + + "WARNING: This action is irreversible and will close screen."); + + first = false; + + // Mod / Demod + using (ImRaii.PushColor(ImGuiCol.Text, + userInfo.IsModerator() ? UIColors.Get("DimRed") : UIColors.Get("PairBlue"))) + { + if (!first) ImGui.SameLine(0f, style.ItemSpacing.X); + + if (_uiSharedService.IconButton(FontAwesomeIcon.UserShield)) + { + userInfo.SetModerator(!userInfo.IsModerator()); + _ = _apiController.GroupSetUserInfo( + new GroupPairUserInfoDto(GroupFullInfo.Group, pair.UserData, userInfo)); + } + } + + UiSharedService.AttachToolTip( + userInfo.IsModerator() ? $"Demod {pair.UserData.AliasOrUID}" : $"Mod {pair.UserData.AliasOrUID}"); + first = false; + } + + if (userInfo == GroupPairUserInfo.None || (!userInfo.IsModerator() && !isUserOwner)) + { + // Pin + using (ImRaii.PushColor(ImGuiCol.Text, + userInfo.IsPinned() ? UIColors.Get("DimRed") : UIColors.Get("PairBlue"))) + { + if (!first) ImGui.SameLine(0f, style.ItemSpacing.X); + + if (_uiSharedService.IconButton(FontAwesomeIcon.Thumbtack)) + { + userInfo.SetPinned(!userInfo.IsPinned()); + + _ = _apiController.GroupSetUserInfo( + new GroupPairUserInfoDto(GroupFullInfo.Group, pair.UserData, userInfo)); + } + } + + UiSharedService.AttachToolTip( + userInfo.IsPinned() ? $"Unpin {pair.UserData.AliasOrUID}" : $"Pin {pair.UserData.AliasOrUID}"); + first = false; + + // Trash + using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed"))) + using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) + { + if (!first) ImGui.SameLine(0f, style.ItemSpacing.X); + + if (_uiSharedService.IconButton(FontAwesomeIcon.Trash)) + { + _ = _apiController.GroupRemoveUser(new GroupPairDto(GroupFullInfo.Group, pair.UserData)); + } + } + + UiSharedService.AttachToolTip($"Remove {pair.UserData.AliasOrUID} from Syncshell" + + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); + first = false; + + // Ban + using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed"))) + using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) + { + if (!first) ImGui.SameLine(0f, style.ItemSpacing.X); + + if (_uiSharedService.IconButton(FontAwesomeIcon.Ban)) + { + Mediator.Publish(new OpenBanUserPopupMessage(pair, GroupFullInfo)); + } + } + + UiSharedService.AttachToolTip($"Ban {pair.UserData.AliasOrUID} from Syncshell" + + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); } } - private void GetTagsFromProfile() + private static void DrawBannedListHeader(float colIdentity, float colMeta) { - if (_profileData != null) + var style = ImGui.GetStyle(); + float x0 = ImGui.GetCursorPosX(); + + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow")); + + // User and reason + ImGui.SetCursorPosX(x0); + ImGui.TextUnformatted("User / Reason"); + + // Moderator and Date + ImGui.SameLine(); + ImGui.SetCursorPosX(x0 + colIdentity + style.ItemSpacing.X); + ImGui.TextUnformatted("Moderator / Date"); + + // Actions + ImGui.SameLine(); + ImGui.SetCursorPosX(x0 + colIdentity + colMeta + style.ItemSpacing.X * 2.0f); + ImGui.TextUnformatted("Actions"); + + ImGui.PopStyleColor(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.0f); + } + + private void DrawBannedRow(BannedGroupUserDto bannedUser, int rowIndex, float colIdentity, float colMeta, float colActions) + { + using var id = ImRaii.PushId("banRow_" + bannedUser.UID); + + var style = ImGui.GetStyle(); + float x0 = ImGui.GetCursorPosX(); + + if (rowIndex % 2 == 0) { - _selectedTags = [.. _profileData.Tags]; + var drawList = ImGui.GetWindowDrawList(); + var pMin = ImGui.GetCursorScreenPos(); + var rowHeight = ImGui.GetTextLineHeightWithSpacing() * 2.6f; + var pMax = new Vector2( + pMin.X + colIdentity + colMeta + colActions + style.ItemSpacing.X * 2.0f, + pMin.Y + rowHeight); + + var bgColor = UIColors.Get("FullBlack").WithAlpha(0.10f); + drawList.AddRectFilled(pMin, pMax, ImGui.ColorConvertFloat4ToU32(bgColor)); } + + ImGui.SetCursorPosX(x0); + ImGui.AlignTextToFramePadding(); + + string alias = bannedUser.UserAlias ?? string.Empty; + string line1 = string.IsNullOrEmpty(alias) + ? bannedUser.UID + : $"{alias} ({bannedUser.UID})"; + + ImGui.TextUnformatted(line1); + + var reason = bannedUser.Reason ?? string.Empty; + if (!string.IsNullOrWhiteSpace(reason)) + { + var reasonPos = new Vector2(x0, ImGui.GetCursorPosY()); + ImGui.SetCursorPos(reasonPos); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); + UiSharedService.TextWrapped(reason); + ImGui.PopStyleColor(); + } + + ImGui.SameLine(); + ImGui.SetCursorPosX(x0 + colIdentity + style.ItemSpacing.X); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"By: {bannedUser.BannedBy}"); + + var dateText = bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); + ImGui.TextUnformatted(dateText); + ImGui.PopStyleColor(); + + ImGui.SameLine(); + ImGui.SetCursorPosX(x0 + colIdentity + colMeta + style.ItemSpacing.X * 2.0f); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Check, "Unban")) + { + _apiController.GroupUnbanUser(bannedUser); + _bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal)); + } + + UiSharedService.AttachToolTip($"Unban {alias} ({bannedUser.UID}) from this Syncshell"); + + ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); + } + private void SavePruneSettings() + { + if (_autoPruneDays <= 0) + { + _autoPruneEnabled = false; + } + + var enabled = _autoPruneEnabled && _autoPruneDays > 0; + var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: enabled, AutoPruneDays: enabled ? _autoPruneDays : 0); + + try + { + _apiController.GroupSetPruneSettings(dto).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save auto prune settings for group {GID}", GroupFullInfo.Group.GID); + UiSharedService.ColorTextWrapped("Failed to save auto-prune settings.", ImGuiColors.DalamudRed); + } + } + + private static bool MatchesUserFilter(Pair pair, string filterLower) + { + var note = pair.GetNote() ?? string.Empty; + var uid = pair.UserData.UID ?? string.Empty; + var alias = pair.UserData.AliasOrUID ?? string.Empty; + + return note.Contains(filterLower, StringComparison.OrdinalIgnoreCase) + || uid.Contains(filterLower, StringComparison.OrdinalIgnoreCase) + || alias.Contains(filterLower, StringComparison.OrdinalIgnoreCase); } public override void OnClose() diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index d7f5605..0586c06 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -1,15 +1,20 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin.Services; +using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto; using LightlessSync.API.Dto.Group; -using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; +using LightlessSync.Services.LightFinder; using LightlessSync.Services.Mediator; +using LightlessSync.UI.Services; +using LightlessSync.UI.Tags; using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; @@ -20,48 +25,59 @@ namespace LightlessSync.UI; public class SyncshellFinderUI : WindowMediatorSubscriberBase { private readonly ApiController _apiController; - private readonly BroadcastService _broadcastService; + private readonly LightFinderService _broadcastService; private readonly UiSharedService _uiSharedService; - private readonly BroadcastScannerService _broadcastScannerService; - private readonly PairManager _pairManager; + private readonly LightFinderScannerService _broadcastScannerService; + private readonly PairUiService _pairUiService; private readonly DalamudUtilService _dalamudUtilService; + private Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f); + private Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f); + + private readonly List _seResolvedSegments = new(); private readonly List _nearbySyncshells = []; private List _currentSyncshells = []; private int _selectedNearbyIndex = -1; + private int _syncshellPageIndex = 0; private readonly HashSet _recentlyJoined = new(StringComparer.Ordinal); private GroupJoinDto? _joinDto; private GroupJoinInfoDto? _joinInfo; private DefaultPermissionsDto _ownPermissions = null!; + private bool _useTestSyncshells = false; + + private bool _compactView = false; + private readonly LightlessProfileManager _lightlessProfileManager; public SyncshellFinderUI( ILogger logger, LightlessMediator mediator, PerformanceCollectorService performanceCollectorService, - BroadcastService broadcastService, + LightFinderService broadcastService, UiSharedService uiShared, ApiController apiController, - BroadcastScannerService broadcastScannerService, - PairManager pairManager, - DalamudUtilService dalamudUtilService) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService) + LightFinderScannerService broadcastScannerService, + PairUiService pairUiService, + DalamudUtilService dalamudUtilService, + LightlessProfileManager lightlessProfileManager) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService) { _broadcastService = broadcastService; _uiSharedService = uiShared; _apiController = apiController; _broadcastScannerService = broadcastScannerService; - _pairManager = pairManager; + _pairUiService = pairUiService; _dalamudUtilService = dalamudUtilService; + _lightlessProfileManager = lightlessProfileManager; IsOpen = false; - SizeConstraints = new() - { - MinimumSize = new(600, 400), - MaximumSize = new(600, 550) - }; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 550)) + .Apply(); Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); + Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false)); + Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false)); } public override async void OnOpen() @@ -72,9 +88,29 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase protected override void DrawInternal() { - _uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("PairBlue")); - _uiSharedService.ColoredSeparator(UIColors.Get("PairBlue")); + ImGui.BeginGroup(); + _uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("LightlessPurple")); +#if DEBUG + if (ImGui.SmallButton("Show test syncshells")) + { + _useTestSyncshells = !_useTestSyncshells; + _ = Task.Run(async () => await RefreshSyncshellsAsync().ConfigureAwait(false)); + } + ImGui.SameLine(); +#endif + + string checkboxLabel = "Compact view"; + float availWidth = ImGui.GetContentRegionAvail().X; + float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight(); + + float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth - 4.0f; + ImGui.SetCursorPosX(rightX); + ImGui.Checkbox(checkboxLabel, ref _compactView); + ImGui.EndGroup(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); if (_nearbySyncshells.Count == 0) { ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted."); @@ -82,17 +118,17 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase if (!_broadcastService.IsBroadcasting) { - _uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow")); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow")); ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder is currently disabled, to locate nearby syncshells, Lightfinder must be active."); ImGuiHelpers.ScaledDummy(0.5f); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f); - ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue")); + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessPurple")); if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0))) { - Mediator.Publish(new UiToggleMessage(typeof(BroadcastUI))); + Mediator.Publish(new UiToggleMessage(typeof(LightFinderUI))); } ImGui.PopStyleColor(); @@ -104,104 +140,536 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase return; } - DrawSyncshellTable(); + string? myHashedCid = null; + try + { + var cid = _dalamudUtilService.GetCID(); + myHashedCid = cid.ToString().GetHash256(); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to get CID, not excluding own broadcast."); + } + var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().Where(b => !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)).ToList() ?? []; + + var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>(); + + foreach (var shell in _nearbySyncshells) + { + string broadcasterName; + + if (shell?.Group == null || string.IsNullOrEmpty(shell.Group.GID)) + continue; + + if (_useTestSyncshells) + { + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) + ? shell.Group.Alias + : shell.Group.GID; + + broadcasterName = $"{displayName} (Tester of TestWorld)"; + } + else + { + var broadcast = broadcasts + .FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal)); + + if (broadcast == null) + continue; + + var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); + if (string.IsNullOrEmpty(name)) + continue; + + var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address); + broadcasterName = !string.IsNullOrEmpty(worldName) + ? $"{name} ({worldName})" + : name; + } + + cardData.Add((shell, broadcasterName)); + } + + if (cardData.Count == 0) + { + ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted."); + return; + } + + if (_compactView) + { + DrawSyncshellGrid(cardData); + } + else + { + DrawSyncshellList(cardData); + } + if (_joinDto != null && _joinInfo != null && _joinInfo.Success) DrawConfirmation(); } - private void DrawSyncshellTable() + private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData) { - if (ImGui.BeginTable("##NearbySyncshellsTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg)) + const int shellsPerPage = 3; + var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage); + if (totalPages <= 0) + totalPages = 1; + + _syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1); + + var firstIndex = _syncshellPageIndex * shellsPerPage; + var lastExclusive = Math.Min(firstIndex + shellsPerPage, listData.Count); + + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f); + ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f); + + for (int index = firstIndex; index < lastExclusive; index++) { - ImGui.TableSetupColumn("Syncshell", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Broadcaster", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale); - ImGui.TableHeadersRow(); + var (shell, broadcasterName) = listData[index]; - foreach (var shell in _nearbySyncshells) + ImGui.PushID(shell.Group.GID); + float rowHeight = 74f * ImGuiHelpers.GlobalScale; + + ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true); + + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; + + var style = ImGui.GetStyle(); + float startX = ImGui.GetCursorPosX(); + float regionW = ImGui.GetContentRegionAvail().X; + float rightTxtW = ImGui.CalcTextSize(broadcasterName).X; + + _uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple")); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Click to open profile."); + if (ImGui.IsItemClicked()) { - // Check if there is an active broadcast for this syncshell, if not, skipping this syncshell - var broadcast = _broadcastScannerService.GetActiveSyncshellBroadcasts() - .FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal)); - - if (broadcast == null) - continue; // no active broadcasts - - var (Name, Address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID); - if (string.IsNullOrEmpty(Name)) - continue; // broadcaster not found in area, skipping - - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - - var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID; - ImGui.TextUnformatted(displayName); - - ImGui.TableNextColumn(); - var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(Address); - var broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{Name} ({worldName})" : Name; - ImGui.TextUnformatted(broadcasterName); - - ImGui.TableNextColumn(); - - var label = $"Join##{shell.Group.GID}"; - ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f)); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f)); - - var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal)); - var isRecentlyJoined = _recentlyJoined.Contains(shell.GID); - - if (!isAlreadyMember && !isRecentlyJoined) - { - if (ImGui.Button(label)) - { - _logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})"); - - _ = Task.Run(async () => - { - try - { - var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto( - shell.Group, - shell.Password, - shell.GroupUserPreferredPermissions - )).ConfigureAwait(false); - - if (info != null && info.Success) - { - _joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions); - _joinInfo = info; - _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; - - _logger.LogInformation($"Fetched join info for {shell.Group.GID}"); - } - else - { - _logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, $"Join failed for {shell.Group.GID}"); - } - }); - } - } - else - { - using (ImRaii.Disabled()) - { - ImGui.Button(label); - } - UiSharedService.AttachToolTip("Already a member or owner of this Syncshell."); - } - ImGui.PopStyleColor(3); + Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group)); } - ImGui.EndTable(); + float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X; + ImGui.SameLine(); + ImGui.SetCursorPosX(rightX); + ImGui.TextUnformatted(broadcasterName); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Broadcaster of the syncshell."); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); + + var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group); + + IReadOnlyList groupTags = + groupProfile != null && groupProfile.Tags.Count > 0 + ? ProfileTagService.ResolveTags(groupProfile.Tags) + : []; + + var limitedTags = groupTags.Count > 3 + ? [.. groupTags.Take(3)] + : groupTags; + + float tagScale = ImGuiHelpers.GlobalScale * 0.9f; + + Vector2 rowStartLocal = ImGui.GetCursorPos(); + + float tagsWidth = 0f; + float tagsHeight = 0f; + + if (limitedTags.Count > 0) + { + (tagsWidth, tagsHeight) = RenderProfileTagsSingleRow(limitedTags, tagScale); + } + else + { + ImGui.SetCursorPosX(startX); + ImGui.TextDisabled("-- No tags set --"); + ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); + } + + float btnBaselineY = rowStartLocal.Y; + float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f); + + ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY)); + DrawJoinButton(shell); + + float btnHeight = ImGui.GetFrameHeightWithSpacing(); + float rowHeightUsed = MathF.Max(tagsHeight, btnHeight); + + ImGui.SetCursorPos(new Vector2( + rowStartLocal.X, + rowStartLocal.Y + rowHeightUsed)); + + ImGui.EndChild(); + ImGui.PopID(); + + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); } + + ImGui.PopStyleVar(2); + + DrawPagination(totalPages); + } + + private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData) + { + const int shellsPerPage = 4; + var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage); + if (totalPages <= 0) + totalPages = 1; + + _syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1); + + var firstIndex = _syncshellPageIndex * shellsPerPage; + var lastExclusive = Math.Min(firstIndex + shellsPerPage, cardData.Count); + + var avail = ImGui.GetContentRegionAvail(); + var spacing = ImGui.GetStyle().ItemSpacing; + + var cardWidth = (avail.X - spacing.X) / 2.0f; + var cardHeight = (avail.Y - spacing.Y - (ImGui.GetFrameHeightWithSpacing() * 2.0f)) / 2.0f; + cardHeight = MathF.Max(110f * ImGuiHelpers.GlobalScale, cardHeight); + + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f); + ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f); + + for (int index = firstIndex; index < lastExclusive; index++) + { + var localIndex = index - firstIndex; + var (shell, broadcasterName) = cardData[index]; + + if (localIndex % 2 != 0) + ImGui.SameLine(); + + ImGui.PushID(shell.Group.GID); + + ImGui.BeginGroup(); + _ = ImGui.BeginChild("ShellCard##" + shell.Group.GID, new Vector2(cardWidth, cardHeight), border: true); + + var displayName = !string.IsNullOrEmpty(shell.Group.Alias) + ? shell.Group.Alias + : shell.Group.GID; + + var style = ImGui.GetStyle(); + float startX = ImGui.GetCursorPosX(); + float availW = ImGui.GetContentRegionAvail().X; + + ImGui.BeginGroup(); + + _uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple")); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Click to open profile."); + if (ImGui.IsItemClicked()) + { + Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group)); + } + + float nameRightX = ImGui.GetItemRectMax().X; + + var regionMinScreen = ImGui.GetCursorScreenPos(); + float regionRightX = regionMinScreen.X + availW; + + float minBroadcasterX = nameRightX + style.ItemSpacing.X; + + float maxBroadcasterWidth = regionRightX - minBroadcasterX; + + string broadcasterToShow = broadcasterName; + + if (!string.IsNullOrEmpty(broadcasterName) && maxBroadcasterWidth > 0f) + { + float bcFullWidth = ImGui.CalcTextSize(broadcasterName).X; + string toolTip; + + if (bcFullWidth > maxBroadcasterWidth) + { + broadcasterToShow = TruncateTextToWidth(broadcasterName, maxBroadcasterWidth); + toolTip = broadcasterName + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell."; + } + else + { + toolTip = "Broadcaster of the syncshell."; + } + + float bcWidth = ImGui.CalcTextSize(broadcasterToShow).X; + + float broadX = regionRightX - bcWidth; + + broadX = MathF.Max(broadX, minBroadcasterX); + + ImGui.SameLine(); + var curPos = ImGui.GetCursorPos(); + ImGui.SetCursorPos(new Vector2(broadX - regionMinScreen.X + startX, curPos.Y + 3f * ImGuiHelpers.GlobalScale)); + ImGui.TextUnformatted(broadcasterToShow); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(toolTip); + } + + ImGui.EndGroup(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); + + ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale)); + + var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group); + + IReadOnlyList groupTags = + groupProfile != null && groupProfile.Tags.Count > 0 + ? ProfileTagService.ResolveTags(groupProfile.Tags) + : []; + + float tagScale = ImGuiHelpers.GlobalScale * 0.9f; + + if (groupTags.Count > 0) + { + var limitedTags = groupTags.Count > 2 + ? [.. groupTags.Take(2)] + : groupTags; + + ImGui.SetCursorPosX(startX); + + var (_, tagsHeight) = RenderProfileTagsSingleRow(limitedTags, tagScale); + + ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); + } + else + { + ImGui.SetCursorPosX(startX); + ImGui.TextDisabled("-- No tags set --"); + ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); + } + + var buttonHeight = ImGui.GetFrameHeightWithSpacing(); + var remainingY = ImGui.GetContentRegionAvail().Y - buttonHeight; + if (remainingY > 0) + ImGui.Dummy(new Vector2(0, remainingY)); + + DrawJoinButton(shell); + + ImGui.EndChild(); + ImGui.EndGroup(); + + ImGui.PopID(); + } + + ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale)); + ImGui.PopStyleVar(2); + + DrawPagination(totalPages); + } + + private void DrawPagination(int totalPages) + { + if (totalPages > 1) + { + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault")); + + var style = ImGui.GetStyle(); + string pageLabel = $"Page {_syncshellPageIndex + 1}/{totalPages}"; + + float prevWidth = ImGui.CalcTextSize("<").X + style.FramePadding.X * 2; + float nextWidth = ImGui.CalcTextSize(">").X + style.FramePadding.X * 2; + float textWidth = ImGui.CalcTextSize(pageLabel).X; + + float totalWidth = prevWidth + textWidth + nextWidth + style.ItemSpacing.X * 2; + + float availWidth = ImGui.GetContentRegionAvail().X; + float offsetX = (availWidth - totalWidth) * 0.5f; + + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offsetX); + + if (ImGui.Button("<##PrevSyncshellPage") && _syncshellPageIndex > 0) + _syncshellPageIndex--; + + ImGui.SameLine(); + ImGui.Text(pageLabel); + + ImGui.SameLine(); + if (ImGui.Button(">##NextSyncshellPage") && _syncshellPageIndex < totalPages - 1) + _syncshellPageIndex++; + } + } + + private void DrawJoinButton(dynamic shell) + { + const string visibleLabel = "Join"; + var label = $"{visibleLabel}##{shell.Group.GID}"; + + var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal)); + var isRecentlyJoined = _recentlyJoined.Contains(shell.GID); + + Vector2 buttonSize; + + if (!_compactView) + { + var style = ImGui.GetStyle(); + var textSize = ImGui.CalcTextSize(visibleLabel); + + var width = textSize.X + style.FramePadding.X * 20f; + buttonSize = new Vector2(width, 30f); + + float availX = ImGui.GetContentRegionAvail().X; + float curX = ImGui.GetCursorPosX(); + float newX = curX + (availX - buttonSize.X); + ImGui.SetCursorPosX(newX); + } + else + { + buttonSize = new Vector2(-1, 0); + } + + if (!isAlreadyMember && !isRecentlyJoined) + { + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f)); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f)); + if (ImGui.Button(label, buttonSize)) + { + _logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})"); + + _ = Task.Run(async () => + { + try + { + var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto( + shell.Group, + shell.Password, + shell.GroupUserPreferredPermissions + )).ConfigureAwait(false); + + if (info != null && info.Success) + { + _joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions); + _joinInfo = info; + _ownPermissions = _apiController.DefaultPermissions.DeepClone()!; + + _logger.LogInformation($"Fetched join info for {shell.Group.GID}"); + } + else + { + _logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Join failed for {shell.Group.GID}"); + } + }); + } + } + else + { + ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("DimRed")); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("DimRed").WithAlpha(0.85f)); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("DimRed").WithAlpha(0.75f)); + + using (ImRaii.Disabled()) + { + ImGui.Button(label, buttonSize); + } + + UiSharedService.AttachToolTip("Already a member or owner of this Syncshell."); + } + + ImGui.PopStyleColor(3); + } + + private (float widthUsed, float rowHeight) RenderProfileTagsSingleRow(IReadOnlyList tags, float scale) + { + if (tags == null || tags.Count == 0) + return (0f, 0f); + + var drawList = ImGui.GetWindowDrawList(); + var style = ImGui.GetStyle(); + var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text); + + var baseLocal = ImGui.GetCursorPos(); + var baseScreen = ImGui.GetCursorScreenPos(); + float availableWidth = ImGui.GetContentRegionAvail().X; + if (availableWidth <= 0f) + availableWidth = 1f; + + float cursorLocalX = baseLocal.X; + float cursorScreenX = baseScreen.X; + float rowHeight = 0f; + + for (int i = 0; i < tags.Count; i++) + { + var tag = tags[i]; + if (!tag.HasContent) + continue; + + var tagSize = ProfileTagRenderer.MeasureTag(tag, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger); + + float tagWidth = tagSize.X; + float tagHeight = tagSize.Y; + + if (cursorLocalX > baseLocal.X && cursorLocalX + tagWidth > baseLocal.X + availableWidth) + break; + + var tagScreenPos = new Vector2(cursorScreenX, baseScreen.Y); + ImGui.SetCursorScreenPos(tagScreenPos); + ImGui.InvisibleButton($"##profileTagInline_{i}", tagSize); + + ProfileTagRenderer.RenderTag(tag, tagScreenPos, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger); + + cursorLocalX += tagWidth + style.ItemSpacing.X; + cursorScreenX += tagWidth + style.ItemSpacing.X; + rowHeight = MathF.Max(rowHeight, tagHeight); + } + + ImGui.SetCursorPos(new Vector2(baseLocal.X, baseLocal.Y + rowHeight)); + + float widthUsed = cursorLocalX - baseLocal.X; + return (widthUsed, rowHeight); + } + private static string TruncateTextToWidth(string text, float maxWidth) + { + if (string.IsNullOrEmpty(text)) + return text; + + const string ellipsis = "..."; + float ellipsisWidth = ImGui.CalcTextSize(ellipsis).X; + + if (maxWidth <= ellipsisWidth) + return ellipsis; + + int low = 0; + int high = text.Length; + string best = ellipsis; + + while (low <= high) + { + int mid = (low + high) / 2; + string candidate = string.Concat(text.AsSpan(0, mid), ellipsis); + float width = ImGui.CalcTextSize(candidate).X; + + if (width <= maxWidth) + { + best = candidate; + low = mid + 1; + } + else + { + high = mid - 1; + } + } + + return best; + } + + private IDalamudTextureWrap? GetIconWrap(uint iconId) + { + try + { + if (_uiSharedService.TryGetIcon(iconId, out var wrap) && wrap != null) + return wrap; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to resolve icon {IconId} for profile tags", iconId); + } + + return null; } private void DrawConfirmation() @@ -228,9 +696,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX); _ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions)); - + _recentlyJoined.Add(_joinDto.Group.GID); - + _joinDto = null; _joinInfo = null; } @@ -263,52 +731,97 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase ImGui.NewLine(); } - private async Task RefreshSyncshellsAsync() + private async Task RefreshSyncshellsAsync(string? gid = null) { var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); - _currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)]; - - _recentlyJoined.RemoveWhere(gid => _currentSyncshells.Any(s => string.Equals(s.GID, gid, StringComparison.Ordinal))); + var snapshot = _pairUiService.GetSnapshot(); + _currentSyncshells = [.. snapshot.GroupPairs.Keys]; - if (syncshellBroadcasts.Count == 0) + _recentlyJoined.RemoveWhere(gid => + _currentSyncshells.Exists(s => string.Equals(s.GID, gid, StringComparison.Ordinal))); + + List? updatedList = []; + + if (_useTestSyncshells) + { + updatedList = BuildTestSyncshells(); + } + else + { + if (syncshellBroadcasts.Count == 0) + { + ClearSyncshells(); + return; + } + + try + { + var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts) + .ConfigureAwait(false); + updatedList = groups?.DistinctBy(g => g.Group.GID).ToList(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to refresh broadcasted syncshells."); + return; + } + } + + if (updatedList == null || updatedList.Count == 0) { ClearSyncshells(); return; } - List? updatedList = []; - try + if (gid != null && _recentlyJoined.Contains(gid)) { - var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false); - updatedList = groups?.ToList(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to refresh broadcasted syncshells."); - return; + _recentlyJoined.Clear(); } - if (updatedList != null) + var previousGid = GetSelectedGid(); + + _nearbySyncshells.Clear(); + _nearbySyncshells.AddRange(updatedList); + + if (previousGid != null) { - var previousGid = GetSelectedGid(); + var newIndex = _nearbySyncshells.FindIndex(s => + string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal)); - _nearbySyncshells.Clear(); - _nearbySyncshells.AddRange(updatedList); - - if (previousGid != null) + if (newIndex >= 0) { - var newIndex = _nearbySyncshells.FindIndex(s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal)); - if (newIndex >= 0) - { - _selectedNearbyIndex = newIndex; - return; - } + _selectedNearbyIndex = newIndex; + return; } } ClearSelection(); } + private static List BuildTestSyncshells() + { + var testGroup1 = new GroupData("TEST-ALPHA", "Alpha Shell"); + var testGroup2 = new GroupData("TEST-BETA", "Beta Shell"); + var testGroup3 = new GroupData("TEST-GAMMA", "Gamma Shell"); + var testGroup4 = new GroupData("TEST-DELTA", "Delta Shell"); + var testGroup5 = new GroupData("TEST-CHARLIE", "Charlie Shell"); + var testGroup6 = new GroupData("TEST-OMEGA", "Omega Shell"); + var testGroup7 = new GroupData("TEST-POINT", "Point Shell"); + var testGroup8 = new GroupData("TEST-HOTEL", "Hotel Shell"); + + return + [ + new(testGroup1, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup2, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup3, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup4, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup5, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup6, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup7, "", GroupUserPreferredPermissions.NoneSet), + new(testGroup8, "", GroupUserPreferredPermissions.NoneSet), + ]; + } + private void ClearSyncshells() { if (_nearbySyncshells.Count == 0) @@ -321,6 +834,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private void ClearSelection() { _selectedNearbyIndex = -1; + _syncshellPageIndex = 0; _joinDto = null; _joinInfo = null; } diff --git a/LightlessSync/UI/Tags/ProfileTagDefinition.cs b/LightlessSync/UI/Tags/ProfileTagDefinition.cs new file mode 100644 index 0000000..fc44aed --- /dev/null +++ b/LightlessSync/UI/Tags/ProfileTagDefinition.cs @@ -0,0 +1,30 @@ +using System.Numerics; + +namespace LightlessSync.UI.Tags; + +public readonly record struct ProfileTagDefinition( + string? Text, + string? SeStringPayload = null, + bool UseTextureSegments = false, + Vector4? BackgroundColor = null, + Vector4? BorderColor = null, + Vector4? TextColor = null) +{ + public bool HasContent => !string.IsNullOrWhiteSpace(Text) || !string.IsNullOrWhiteSpace(SeStringPayload); + public bool HasSeString => !string.IsNullOrWhiteSpace(SeStringPayload); + + public ProfileTagDefinition WithColors(Vector4? background, Vector4? border, Vector4? textColor = null) + => this with { BackgroundColor = background, BorderColor = border, TextColor = textColor }; + + public static ProfileTagDefinition FromText(string text, Vector4? background = null, Vector4? border = null, Vector4? textColor = null) + => new(text, null, false, background, border, textColor); + + public static ProfileTagDefinition FromIcon(uint iconId, Vector4? background = null, Vector4? border = null) + => new(null, $"", true, background, border, null); + + public static ProfileTagDefinition FromIconAndText(uint iconId, string text, Vector4? background = null, Vector4? border = null, Vector4? textColor = null) + => new(text, $" {text}", true, background, border, textColor); + + public static ProfileTagDefinition FromSeString(string payload, Vector4? background = null, Vector4? border = null, Vector4? textColor = null) + => new(null, payload, true, background, border, textColor); +} diff --git a/LightlessSync/UI/Tags/ProfileTagRenderer.cs b/LightlessSync/UI/Tags/ProfileTagRenderer.cs new file mode 100644 index 0000000..28f0295 --- /dev/null +++ b/LightlessSync/UI/Tags/ProfileTagRenderer.cs @@ -0,0 +1,224 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.ImGuiSeStringRenderer; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Interface.Utility; +using LightlessSync.Utils; +using Microsoft.Extensions.Logging; +using System.Numerics; + +namespace LightlessSync.UI.Tags; + +internal static class ProfileTagRenderer +{ + public static Vector2 MeasureTag( + ProfileTagDefinition tag, + float scale, + ImGuiStylePtr style, + Vector4 fallbackBackground, + Vector4 fallbackBorder, + uint defaultTextColorU32, + List segmentBuffer, + Func iconResolver, + ILogger? logger) + => RenderTagInternal(tag, Vector2.Zero, scale, default, style, fallbackBackground, fallbackBorder, defaultTextColorU32, segmentBuffer, iconResolver, logger, draw: false); + + public static Vector2 RenderTag( + ProfileTagDefinition tag, + Vector2 screenMin, + float scale, + ImDrawListPtr drawList, + ImGuiStylePtr style, + Vector4 fallbackBackground, + Vector4 fallbackBorder, + uint defaultTextColorU32, + List segmentBuffer, + Func iconResolver, + ILogger? logger) + => RenderTagInternal(tag, screenMin, scale, drawList, style, fallbackBackground, fallbackBorder, defaultTextColorU32, segmentBuffer, iconResolver, logger, draw: true); + + private static Vector2 RenderTagInternal( + ProfileTagDefinition tag, + Vector2 screenMin, + float scale, + ImDrawListPtr drawList, + ImGuiStylePtr style, + Vector4 fallbackBackground, + Vector4 fallbackBorder, + uint defaultTextColorU32, + List segmentBuffer, + Func iconResolver, + ILogger? logger, + bool draw) + { + segmentBuffer.Clear(); + + var padding = new Vector2(10f * scale, 6f * scale); + var rounding = style.FrameRounding > 0f ? style.FrameRounding : 6f * scale; + + var backgroundColor = tag.BackgroundColor ?? fallbackBackground; + var borderColor = tag.BorderColor ?? fallbackBorder; + var textColor = tag.TextColor ?? style.Colors[(int)ImGuiCol.Text]; + var textColorU32 = tag.TextColor.HasValue ? ImGui.ColorConvertFloat4ToU32(tag.TextColor.Value) : defaultTextColorU32; + + string? textContent = tag.Text; + Vector2 textSize = string.IsNullOrWhiteSpace(textContent) ? Vector2.Zero : ImGui.CalcTextSize(textContent); + + var sePayload = tag.SeStringPayload; + bool hasSeString = !string.IsNullOrWhiteSpace(sePayload); + bool useTextureSegments = hasSeString && tag.UseTextureSegments; + bool useSeRenderer = hasSeString && !useTextureSegments; + Vector2 seSize = Vector2.Zero; + List? seSegments = null; + + if (hasSeString) + { + if (useSeRenderer) + { + try + { + var drawParams = new SeStringDrawParams + { + TargetDrawList = draw ? drawList : default, + ScreenOffset = draw ? screenMin + padding : Vector2.Zero, + WrapWidth = float.MaxValue + }; + + var measure = ImGuiHelpers.CompileSeStringWrapped(sePayload!, drawParams); + seSize = measure.Size; + if (seSize.Y <= 0f) + seSize.Y = ImGui.GetTextLineHeight(); + + textContent = null; + textSize = Vector2.Zero; + } + catch (Exception ex) + { + logger?.LogDebug(ex, "Failed to compile SeString payload '{Payload}' for profile tag", sePayload); + useSeRenderer = false; + } + } + + if (!useSeRenderer && useTextureSegments) + { + segmentBuffer.Clear(); + if (SeStringUtils.TryResolveSegments(sePayload!, scale, iconResolver, segmentBuffer, out seSize) && segmentBuffer.Count > 0) + { + seSegments = segmentBuffer; + textContent = null; + textSize = Vector2.Zero; + } + else + { + segmentBuffer.Clear(); + var fallback = SeStringUtils.StripMarkup(sePayload!); + if (!string.IsNullOrWhiteSpace(fallback)) + { + textContent = fallback; + textSize = ImGui.CalcTextSize(fallback); + } + } + } + else if (!useSeRenderer && string.IsNullOrWhiteSpace(textContent)) + { + var fallback = SeStringUtils.StripMarkup(sePayload!); + if (!string.IsNullOrWhiteSpace(fallback)) + { + textContent = fallback; + textSize = ImGui.CalcTextSize(fallback); + } + } + } + + bool drewSeString = useSeRenderer || seSegments is { Count: > 0 }; + var contentHeight = drewSeString ? seSize.Y : textSize.Y; + if (contentHeight <= 0f) + contentHeight = ImGui.GetTextLineHeight(); + + var contentWidth = drewSeString ? seSize.X : textSize.X; + if (contentWidth <= 0f) + contentWidth = textSize.X; + if (contentWidth <= 0f) + contentWidth = 40f * scale; + + var tagSize = new Vector2(contentWidth + padding.X * 2f, contentHeight + padding.Y * 2f); + + if (!draw) + { + if (seSegments is not null) + seSegments.Clear(); + return tagSize; + } + + var rectMin = screenMin; + var rectMax = rectMin + tagSize; + drawList.AddRectFilled(rectMin, rectMax, ImGui.ColorConvertFloat4ToU32(backgroundColor), rounding); + drawList.AddRect(rectMin, rectMax, ImGui.ColorConvertFloat4ToU32(borderColor), rounding); + + var contentStart = rectMin + padding; + var verticalOffset = (tagSize.Y - padding.Y * 2f - contentHeight) * 0.5f; + var basePos = new Vector2(contentStart.X, contentStart.Y + MathF.Max(verticalOffset, 0f)); + + if (useSeRenderer && sePayload is { Length: > 0 }) + { + var drawParams = new SeStringDrawParams + { + TargetDrawList = drawList, + ScreenOffset = basePos, + WrapWidth = float.MaxValue + }; + + try + { + ImGuiHelpers.CompileSeStringWrapped(sePayload!, drawParams); + } + catch (Exception ex) + { + logger?.LogDebug(ex, "Failed to draw SeString payload '{Payload}' for profile tag", sePayload); + var fallback = !string.IsNullOrWhiteSpace(textContent) ? textContent : SeStringUtils.StripMarkup(sePayload!); + if (!string.IsNullOrWhiteSpace(fallback)) + drawList.AddText(basePos, textColorU32, fallback); + } + } + else if (seSegments is { Count: > 0 }) + { + var segmentX = basePos.X; + foreach (var segment in seSegments) + { + var segmentPos = new Vector2(segmentX, basePos.Y + (contentHeight - segment.Size.Y) * 0.5f); + switch (segment.Type) + { + case SeStringUtils.SeStringSegmentType.Icon: + if (segment.Texture != null) + { + drawList.AddImage(segment.Texture.Handle, segmentPos, segmentPos + segment.Size); + } + else if (!string.IsNullOrEmpty(segment.Text)) + { + drawList.AddText(segmentPos, textColorU32, segment.Text); + } + break; + case SeStringUtils.SeStringSegmentType.Text: + var colorU32 = segment.Color.HasValue + ? ImGui.ColorConvertFloat4ToU32(segment.Color.Value) + : textColorU32; + drawList.AddText(segmentPos, colorU32, segment.Text ?? string.Empty); + break; + } + + segmentX += segment.Size.X; + } + + seSegments.Clear(); + } + else if (!string.IsNullOrWhiteSpace(textContent)) + { + drawList.AddText(basePos, textColorU32, textContent); + } + else + { + drawList.AddText(basePos, textColorU32, string.Empty); + } + + return tagSize; + } +} diff --git a/LightlessSync/UI/Tags/ProfileTagService.cs b/LightlessSync/UI/Tags/ProfileTagService.cs new file mode 100644 index 0000000..d340d76 --- /dev/null +++ b/LightlessSync/UI/Tags/ProfileTagService.cs @@ -0,0 +1,170 @@ +using System.Numerics; + +namespace LightlessSync.UI.Tags; + +/// +/// Library of tags. That's it. +/// +public sealed class ProfileTagService +{ + private static readonly IReadOnlyDictionary TagLibrary = CreateTagLibrary(); + + public static IReadOnlyDictionary GetTagLibrary() + => TagLibrary; + + public static IReadOnlyList ResolveTags(IReadOnlyList? tagIds) + { + if (tagIds is null || tagIds.Count == 0) + return Array.Empty(); + + var result = new List(tagIds.Count); + foreach (var id in tagIds) + { + if (TagLibrary.TryGetValue(id, out var tag)) + result.Add(tag); + } + + return result; + } + + public bool TryGetDefinition(int tagId, out ProfileTagDefinition definition) + => TagLibrary.TryGetValue(tagId, out definition); + + private static IReadOnlyDictionary CreateTagLibrary() + { + return new Dictionary + { + [0] = ProfileTagDefinition.FromIconAndText( + 230419, + "SFW", + background: new Vector4(0.16f, 0.24f, 0.18f, 0.95f), + border: new Vector4(0.32f, 0.52f, 0.34f, 0.85f), + textColor: new Vector4(0.78f, 0.94f, 0.80f, 1f)), + + [1] = ProfileTagDefinition.FromIconAndText( + 230419, + "NSFW", + background: new Vector4(0.32f, 0.18f, 0.22f, 0.95f), + border: new Vector4(0.72f, 0.32f, 0.38f, 0.85f), + textColor: new Vector4(1f, 0.82f, 0.86f, 1f)), + + + [2] = ProfileTagDefinition.FromIconAndText( + 61545, + "RP", + background: new Vector4(0.20f, 0.20f, 0.30f, 0.95f), + border: new Vector4(0.42f, 0.42f, 0.66f, 0.85f), + textColor: new Vector4(0.80f, 0.84f, 1f, 1f)), + + [3] = ProfileTagDefinition.FromIconAndText( + 61545, + "ERP", + background: new Vector4(0.20f, 0.20f, 0.30f, 0.95f), + border: new Vector4(0.42f, 0.42f, 0.66f, 0.85f), + textColor: new Vector4(0.80f, 0.84f, 1f, 1f)), + + [4] = ProfileTagDefinition.FromIconAndText( + 230420, + "No RP", + background: new Vector4(0.30f, 0.18f, 0.30f, 0.95f), + border: new Vector4(0.69f, 0.40f, 0.65f, 0.85f), + textColor: new Vector4(1f, 0.84f, 1f, 1f)), + + [5] = ProfileTagDefinition.FromIconAndText( + 230420, + "No ERP", + background: new Vector4(0.30f, 0.18f, 0.30f, 0.95f), + border: new Vector4(0.69f, 0.40f, 0.65f, 0.85f), + textColor: new Vector4(1f, 0.84f, 1f, 1f)), + + + [6] = ProfileTagDefinition.FromIconAndText( + 60756, + "Venues", + background: new Vector4(0.18f, 0.24f, 0.28f, 0.95f), + border: new Vector4(0.33f, 0.55f, 0.63f, 0.85f), + textColor: new Vector4(0.78f, 0.90f, 0.97f, 1f)), + + [7] = ProfileTagDefinition.FromIconAndText( + 61546, + "GPose", + background: new Vector4(0.18f, 0.18f, 0.26f, 0.95f), + border: new Vector4(0.35f, 0.34f, 0.54f, 0.85f), + textColor: new Vector4(0.80f, 0.82f, 0.96f, 1f)), + + + [8] = ProfileTagDefinition.FromIconAndText( + 60572, + "Limsa"), + + [9] = ProfileTagDefinition.FromIconAndText( + 60573, + "Gridania"), + + [10] = ProfileTagDefinition.FromIconAndText( + 60574, + "Ul'dah"), + + + [11] = ProfileTagDefinition.FromIconAndText( + 61397, + "WU/T"), + + + [1001] = ProfileTagDefinition.FromIcon(61806), // PVP + [1002] = ProfileTagDefinition.FromIcon(61832), // Ultimate + [1003] = ProfileTagDefinition.FromIcon(61802), // Raids + [1004] = ProfileTagDefinition.FromIcon(61807), // Roulette + [1005] = ProfileTagDefinition.FromIcon(61816), // Crafting + [1006] = ProfileTagDefinition.FromIcon(61753), // Casual + [1007] = ProfileTagDefinition.FromIcon(61754), // Hardcore + [1008] = ProfileTagDefinition.FromIcon(61759), // Glamour + [1009] = ProfileTagDefinition.FromIcon(61760), // Mentor + + // Role Tags + [2001] = ProfileTagDefinition.FromIconAndText(62581, "Tank"), + [2002] = ProfileTagDefinition.FromIconAndText(62582, "Healer"), + [2003] = ProfileTagDefinition.FromIconAndText(62583, "DPS"), + [2004] = ProfileTagDefinition.FromIconAndText(62584, "Melee DPS"), + [2005] = ProfileTagDefinition.FromIconAndText(62585, "Ranged DPS"), + [2006] = ProfileTagDefinition.FromIconAndText(62586, "Physical Ranged DPS"), + [2007] = ProfileTagDefinition.FromIconAndText(62587, "Magical Ranged DPS"), + + // Misc Role Tags + [2101] = ProfileTagDefinition.FromIconAndText(62146, "All-Rounder"), + + // Tank Job Tags + [2201] = ProfileTagDefinition.FromIconAndText(62119, "Paladin"), + [2202] = ProfileTagDefinition.FromIconAndText(62121, "Warrior"), + [2203] = ProfileTagDefinition.FromIconAndText(62132, "Dark Knight"), + [2204] = ProfileTagDefinition.FromIconAndText(62137, "Gunbreaker"), + + // Healer Job Tags + [2301] = ProfileTagDefinition.FromIconAndText(62124, "White Mage"), + [2302] = ProfileTagDefinition.FromIconAndText(62128, "Scholar"), + [2303] = ProfileTagDefinition.FromIconAndText(62133, "Astrologian"), + [2304] = ProfileTagDefinition.FromIconAndText(62140, "Sage"), + + // Melee DPS Job Tags + [2401] = ProfileTagDefinition.FromIconAndText(62120, "Monk"), + [2402] = ProfileTagDefinition.FromIconAndText(62122, "Dragoon"), + [2403] = ProfileTagDefinition.FromIconAndText(62130, "Ninja"), + [2404] = ProfileTagDefinition.FromIconAndText(62134, "Samurai"), + [2405] = ProfileTagDefinition.FromIconAndText(62139, "Reaper"), + [2406] = ProfileTagDefinition.FromIconAndText(62141, "Viper"), + + // PRanged DPS Job Tags + [2501] = ProfileTagDefinition.FromIconAndText(62123, "Bard"), + [2502] = ProfileTagDefinition.FromIconAndText(62131, "Machinist"), + [2503] = ProfileTagDefinition.FromIconAndText(62138, "Dancer"), + + // MRanged DPS Job Tags + [2601] = ProfileTagDefinition.FromIconAndText(62125, "Black Mage"), + [2602] = ProfileTagDefinition.FromIconAndText(62127, "Summoner"), + [2603] = ProfileTagDefinition.FromIconAndText(62135, "Red Mage"), + [2604] = ProfileTagDefinition.FromIconAndText(62142, "Pictomancer"), + [2605] = ProfileTagDefinition.FromIconAndText(62136, "Blue Mage") // this job sucks xd + + }; + } +} diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index b4327c0..cc69a5d 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -2,18 +2,21 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin.Services; using Dalamud.Utility; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.LightlessConfiguration.Models; -using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; +using LightlessSync.Services.LightFinder; using LightlessSync.Services.Mediator; +using LightlessSync.UI.Models; +using LightlessSync.UI.Style; using LightlessSync.Utils; using LightlessSync.WebAPI; +using System; using System.Numerics; - namespace LightlessSync.UI; public class TopTabMenu @@ -22,9 +25,10 @@ public class TopTabMenu private readonly LightlessMediator _lightlessMediator; - private readonly PairManager _pairManager; private readonly PairRequestService _pairRequestService; private readonly DalamudUtilService _dalamudUtilService; + private readonly LightFinderService _lightFinderService; + private readonly LightFinderScannerService _lightFinderScannerService; private readonly HashSet _pendingPairRequestActions = new(StringComparer.Ordinal); private bool _pairRequestsExpanded; // useless for now private int _lastRequestCount; @@ -36,15 +40,18 @@ public class TopTabMenu private string _pairToAdd = string.Empty; private SelectedTab _selectedTab = SelectedTab.None; - public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) + private PairUiSnapshot? _currentSnapshot; + + public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService, LightFinderService lightFinderService, LightFinderScannerService lightFinderScannerService) { _lightlessMediator = lightlessMediator; _apiController = apiController; - _pairManager = pairManager; _pairRequestService = pairRequestService; _dalamudUtilService = dalamudUtilService; _uiSharedService = uiSharedService; _lightlessNotificationService = lightlessNotificationService; + _lightFinderService = lightFinderService; + _lightFinderScannerService = lightFinderScannerService; } private enum SelectedTab @@ -77,130 +84,178 @@ public class TopTabMenu _selectedTab = value; } } - public void Draw() + + private PairUiSnapshot Snapshot => _currentSnapshot ?? throw new InvalidOperationException("Pair UI snapshot is not available outside of Draw."); + + public void Draw(PairUiSnapshot snapshot) { - var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; - var spacing = ImGui.GetStyle().ItemSpacing; - var buttonX = (availableWidth - (spacing.X * 4)) / 5f; - var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y; - var buttonSize = new Vector2(buttonX, buttonY); - var drawList = ImGui.GetWindowDrawList(); - var underlineColor = ImGui.GetColorU32(UIColors.Get("LightlessPurpleActive")); // ImGui.GetColorU32(ImGuiCol.Separator); - var btncolor = ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0))); - - ImGuiHelpers.ScaledDummy(spacing.Y / 2f); - - using (ImRaii.PushFont(UiBuilder.IconFont)) + _currentSnapshot = snapshot; + try { - var x = ImGui.GetCursorScreenPos(); - if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), buttonSize)) + var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + var spacing = ImGui.GetStyle().ItemSpacing; + var buttonX = (availableWidth - (spacing.X * 5)) / 6f; + var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y; + var buttonSize = new Vector2(buttonX, buttonY); + const float buttonBorderThickness = 12f; + var buttonRounding = ImGui.GetStyle().FrameRounding; + var drawList = ImGui.GetWindowDrawList(); + var underlineColor = ImGui.GetColorU32(UIColors.Get("LightlessPurpleActive")); // ImGui.GetColorU32(ImGuiCol.Separator); + var btncolor = ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0))); + + ImGuiHelpers.ScaledDummy(spacing.Y / 2f); + + using (ImRaii.PushFont(UiBuilder.IconFont)) { - TabSelection = TabSelection == SelectedTab.Individual ? SelectedTab.None : SelectedTab.Individual; + var x = ImGui.GetCursorScreenPos(); + if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), buttonSize)) + { + TabSelection = TabSelection == SelectedTab.Individual ? SelectedTab.None : SelectedTab.Individual; + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive()) + { + Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding); + } + ImGui.SameLine(); + var xAfter = ImGui.GetCursorScreenPos(); + if (TabSelection == SelectedTab.Individual) + drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, + xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, + underlineColor, 2); } + UiSharedService.AttachToolTip("Individual Pair Menu"); + + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + var x = ImGui.GetCursorScreenPos(); + if (ImGui.Button(FontAwesomeIcon.Users.ToIconString(), buttonSize)) + { + TabSelection = TabSelection == SelectedTab.Syncshell ? SelectedTab.None : SelectedTab.Syncshell; + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive()) + { + Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding); + } + ImGui.SameLine(); + var xAfter = ImGui.GetCursorScreenPos(); + if (TabSelection == SelectedTab.Syncshell) + drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, + xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, + underlineColor, 2); + } + UiSharedService.AttachToolTip("Syncshell Menu"); + + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.Comments.ToIconString(), buttonSize)) + { + _lightlessMediator.Publish(new UiToggleMessage(typeof(ZoneChatUi))); + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive()) + { + Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding); + } + } + UiSharedService.AttachToolTip("Lightless Chat"); ImGui.SameLine(); - var xAfter = ImGui.GetCursorScreenPos(); + + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + var x = ImGui.GetCursorScreenPos(); + if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize)) + { + TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder; + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive()) + { + Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding); + } + + ImGui.SameLine(); + var xAfter = ImGui.GetCursorScreenPos(); + if (TabSelection == SelectedTab.Lightfinder) + drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, + xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, + underlineColor, 2); + } + UiSharedService.AttachToolTip("Lightfinder"); + + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + var x = ImGui.GetCursorScreenPos(); + if (ImGui.Button(FontAwesomeIcon.UserCog.ToIconString(), buttonSize)) + { + TabSelection = TabSelection == SelectedTab.UserConfig ? SelectedTab.None : SelectedTab.UserConfig; + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive()) + { + Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding); + } + + ImGui.SameLine(); + var xAfter = ImGui.GetCursorScreenPos(); + if (TabSelection == SelectedTab.UserConfig) + drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, + xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, + underlineColor, 2); + } + UiSharedService.AttachToolTip("Your User Menu"); + + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + var x = ImGui.GetCursorScreenPos(); + if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize)) + { + _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive()) + { + Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding); + } + ImGui.SameLine(); + } + UiSharedService.AttachToolTip("Open Lightless Settings"); + + ImGui.NewLine(); + btncolor.Dispose(); + + ImGuiHelpers.ScaledDummy(spacing); + if (TabSelection == SelectedTab.Individual) - drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, - xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, - underlineColor, 2); - } - UiSharedService.AttachToolTip("Individual Pair Menu"); - - using (ImRaii.PushFont(UiBuilder.IconFont)) - { - var x = ImGui.GetCursorScreenPos(); - if (ImGui.Button(FontAwesomeIcon.Users.ToIconString(), buttonSize)) { - TabSelection = TabSelection == SelectedTab.Syncshell ? SelectedTab.None : SelectedTab.Syncshell; + DrawAddPair(availableWidth, spacing.X); + DrawGlobalIndividualButtons(availableWidth, spacing.X); } - ImGui.SameLine(); - var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.Syncshell) - drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, - xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, - underlineColor, 2); - } - UiSharedService.AttachToolTip("Syncshell Menu"); - - ImGui.SameLine(); - using (ImRaii.PushFont(UiBuilder.IconFont)) - { - var x = ImGui.GetCursorScreenPos(); - if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize)) + else if (TabSelection == SelectedTab.Syncshell) { - TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder; + DrawSyncshellMenu(availableWidth, spacing.X); + DrawGlobalSyncshellButtons(availableWidth, spacing.X); + } + else if (TabSelection == SelectedTab.Lightfinder) + { + DrawLightfinderMenu(availableWidth, spacing.X); + } + else if (TabSelection == SelectedTab.UserConfig) + { + DrawUserConfig(availableWidth, spacing.X); } - ImGui.SameLine(); - var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.Lightfinder) - drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, - xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, - underlineColor, 2); - } - UiSharedService.AttachToolTip("Lightfinder"); + if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); - ImGui.SameLine(); - using (ImRaii.PushFont(UiBuilder.IconFont)) + + DrawIncomingPairRequests(availableWidth); + + ImGui.Separator(); + + DrawFilter(availableWidth, spacing.X); + } + finally { - var x = ImGui.GetCursorScreenPos(); - if (ImGui.Button(FontAwesomeIcon.UserCog.ToIconString(), buttonSize)) - { - TabSelection = TabSelection == SelectedTab.UserConfig ? SelectedTab.None : SelectedTab.UserConfig; - } - - ImGui.SameLine(); - var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.UserConfig) - drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, - xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, - underlineColor, 2); + _currentSnapshot = null; } - UiSharedService.AttachToolTip("Your User Menu"); - - ImGui.SameLine(); - using (ImRaii.PushFont(UiBuilder.IconFont)) - { - var x = ImGui.GetCursorScreenPos(); - if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize)) - { - _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); - } - ImGui.SameLine(); - } - UiSharedService.AttachToolTip("Open Lightless Settings"); - - ImGui.NewLine(); - btncolor.Dispose(); - - ImGuiHelpers.ScaledDummy(spacing); - - if (TabSelection == SelectedTab.Individual) - { - DrawAddPair(availableWidth, spacing.X); - DrawGlobalIndividualButtons(availableWidth, spacing.X); - } - else if (TabSelection == SelectedTab.Syncshell) - { - DrawSyncshellMenu(availableWidth, spacing.X); - DrawGlobalSyncshellButtons(availableWidth, spacing.X); - } - else if (TabSelection == SelectedTab.Lightfinder) - { - DrawLightfinderMenu(availableWidth, spacing.X); - } - else if (TabSelection == SelectedTab.UserConfig) - { - DrawUserConfig(availableWidth, spacing.X); - } - - if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); - - DrawIncomingPairRequests(availableWidth); - - ImGui.Separator(); - - DrawFilter(availableWidth, spacing.X); } private void DrawAddPair(float availableXWidth, float spacingX) @@ -209,7 +264,7 @@ public class TopTabMenu ImGui.SetNextItemWidth(availableXWidth - buttonSize - spacingX); ImGui.InputTextWithHint("##otheruid", "Other players UID/Alias", ref _pairToAdd, 20); ImGui.SameLine(); - var alreadyExisting = _pairManager.DirectPairs.Exists(p => string.Equals(p.UserData.UID, _pairToAdd, StringComparison.Ordinal) || string.Equals(p.UserData.Alias, _pairToAdd, StringComparison.Ordinal)); + var alreadyExisting = Snapshot.DirectPairs.Any(p => string.Equals(p.UserData.UID, _pairToAdd, StringComparison.Ordinal) || string.Equals(p.UserData.Alias, _pairToAdd, StringComparison.Ordinal)); using (ImRaii.Disabled(alreadyExisting || string.IsNullOrEmpty(_pairToAdd))) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, "Add")) @@ -431,12 +486,23 @@ public class TopTabMenu { Filter = filter; } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive()) + { + Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, 10, exactSize: true, clipToElement: true, roundingOverride: ImGui.GetStyle().FrameRounding); + } ImGui.SameLine(); - using var disabled = ImRaii.Disabled(string.IsNullOrEmpty(Filter)); + var disableClear = string.IsNullOrEmpty(Filter); + using var disabled = ImRaii.Disabled(disableClear); + var clearHovered = false; if (_uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Clear")) { Filter = string.Empty; } + clearHovered = ImGui.IsItemHovered(); + if (!disableClear && clearHovered) + { + Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, 10, exactSize: true, clipToElement: true, roundingOverride: ImGui.GetStyle().FrameRounding); + } } private void DrawGlobalIndividualButtons(float availableXWidth, float spacingX) @@ -666,7 +732,7 @@ public class TopTabMenu if (ImGui.Button(FontAwesomeIcon.Check.ToIconString(), buttonSize)) { _ = GlobalControlCountdown(10); - var bulkSyncshells = _pairManager.GroupPairs.Keys.OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase) + var bulkSyncshells = Snapshot.GroupPairs.Keys.OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Group.GID, g => { var perm = g.GroupUserPermissions; @@ -691,7 +757,8 @@ public class TopTabMenu { var buttonX = (availableWidth - (spacingX)) / 2f; - using (ImRaii.Disabled(_pairManager.GroupPairs.Select(k => k.Key).Distinct() + using (ImRaii.Disabled(Snapshot.GroupPairs.Keys + .Distinct() .Count(g => string.Equals(g.OwnerUID, _apiController.UID, StringComparison.Ordinal)) >= _apiController.ServerInfo.MaxGroupsCreatedByUser)) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Create new Syncshell", buttonX)) @@ -701,7 +768,7 @@ public class TopTabMenu ImGui.SameLine(); } - using (ImRaii.Disabled(_pairManager.GroupPairs.Select(k => k.Key).Distinct().Count() >= _apiController.ServerInfo.MaxGroupsJoinedByUser)) + using (ImRaii.Disabled(Snapshot.GroupPairs.Keys.Distinct().Count() >= _apiController.ServerInfo.MaxGroupsJoinedByUser)) { if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Join existing Syncshell", buttonX)) { @@ -716,17 +783,46 @@ public class TopTabMenu if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, "Lightfinder", buttonX, center: true)) { - _lightlessMediator.Publish(new UiToggleMessage(typeof(BroadcastUI))); + _lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI))); } ImGui.SameLine(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Globe, "Syncshell Finder", buttonX, center: true)) + var syncshellFinderLabel = GetSyncshellFinderLabel(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Globe, syncshellFinderLabel, buttonX, center: true)) { _lightlessMediator.Publish(new UiToggleMessage(typeof(SyncshellFinderUI))); } } + private string GetSyncshellFinderLabel() + { + if (!_lightFinderService.IsBroadcasting) + return "Syncshell Finder"; + + string? myHashedCid = null; + try + { + var cid = _dalamudUtilService.GetCID(); + myHashedCid = cid.ToString().GetHash256(); + } + catch (Exception) + { + // Couldnt get own CID, log and return default table + } + + var nearbyCount = _lightFinderScannerService + .GetActiveSyncshellBroadcasts() + .Where(b => + !string.IsNullOrEmpty(b.GID) && + !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)) + .Select(b => b.GID!) + .Distinct(StringComparer.Ordinal) + .Count(); + + return nearbyCount > 0 ? $"Syncshell Finder ({nearbyCount})" : "Syncshell Finder"; + } + private void DrawUserConfig(float availableWidth, float spacingX) { var buttonX = (availableWidth - spacingX) / 2f; @@ -770,7 +866,7 @@ public class TopTabMenu if (_uiSharedService.IconTextButton(enableIcon, enableText, null, true)) { _ = GlobalControlCountdown(10); - var bulkIndividualPairs = _pairManager.PairsWithGroups.Keys + var bulkIndividualPairs = Snapshot.PairsWithGroups.Keys .Where(g => g.IndividualPairStatus == IndividualPairStatus.Bidirectional) .ToDictionary(g => g.UserPair.User.UID, g => { @@ -784,7 +880,7 @@ public class TopTabMenu if (_uiSharedService.IconTextButton(disableIcon, disableText, null, true)) { _ = GlobalControlCountdown(10); - var bulkIndividualPairs = _pairManager.PairsWithGroups.Keys + var bulkIndividualPairs = Snapshot.PairsWithGroups.Keys .Where(g => g.IndividualPairStatus == IndividualPairStatus.Bidirectional) .ToDictionary(g => g.UserPair.User.UID, g => { @@ -808,7 +904,7 @@ public class TopTabMenu if (_uiSharedService.IconTextButton(enableIcon, enableText, null, true)) { _ = GlobalControlCountdown(10); - var bulkSyncshells = _pairManager.GroupPairs.Keys + var bulkSyncshells = Snapshot.GroupPairs.Keys .OrderBy(u => u.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Group.GID, g => { @@ -822,7 +918,7 @@ public class TopTabMenu if (_uiSharedService.IconTextButton(disableIcon, disableText, null, true)) { _ = GlobalControlCountdown(10); - var bulkSyncshells = _pairManager.GroupPairs.Keys + var bulkSyncshells = Snapshot.GroupPairs.Keys .OrderBy(u => u.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Group.GID, g => { diff --git a/LightlessSync/UI/UIColors.cs b/LightlessSync/UI/UIColors.cs index 3c1eabd..9d7f770 100644 --- a/LightlessSync/UI/UIColors.cs +++ b/LightlessSync/UI/UIColors.cs @@ -15,8 +15,11 @@ namespace LightlessSync.UI { "FullBlack", "#000000" }, { "LightlessBlue", "#a6c2ff" }, { "LightlessYellow", "#ffe97a" }, + { "LightlessYellow2", "#cfbd63" }, { "LightlessGreen", "#7cd68a" }, + { "LightlessGreenDefault", "#468a50" }, { "LightlessOrange", "#ffb366" }, + { "LightlessGrey", "#8f8f8f" }, { "PairBlue", "#88a2db" }, { "DimRed", "#d44444" }, { "LightlessAdminText", "#ffd663" }, @@ -25,6 +28,9 @@ namespace LightlessSync.UI { "Lightfinder", "#ad8af5" }, { "LightfinderEdge", "#000000" }, + + { "ProfileBodyGradientTop", "#2f283fff" }, + { "ProfileBodyGradientBottom", "#372d4d00" }, }; private static LightlessConfigService? _configService; @@ -40,7 +46,7 @@ namespace LightlessSync.UI return HexToRgba(customColorHex); if (!DefaultHexColors.TryGetValue(name, out var hex)) - throw new ArgumentException($"Color '{name}' not found in UIColors."); + throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name)); return HexToRgba(hex); } @@ -48,7 +54,7 @@ namespace LightlessSync.UI public static void Set(string name, Vector4 color) { if (!DefaultHexColors.ContainsKey(name)) - throw new ArgumentException($"Color '{name}' not found in UIColors."); + throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name)); if (_configService != null) { @@ -78,7 +84,7 @@ namespace LightlessSync.UI public static Vector4 GetDefault(string name) { if (!DefaultHexColors.TryGetValue(name, out var hex)) - throw new ArgumentException($"Color '{name}' not found in UIColors."); + throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name)); return HexToRgba(hex); } @@ -96,10 +102,10 @@ namespace LightlessSync.UI public static Vector4 HexToRgba(string hexColor) { hexColor = hexColor.TrimStart('#'); - int r = int.Parse(hexColor.Substring(0, 2), NumberStyles.HexNumber); - int g = int.Parse(hexColor.Substring(2, 2), NumberStyles.HexNumber); - int b = int.Parse(hexColor.Substring(4, 2), NumberStyles.HexNumber); - int a = hexColor.Length == 8 ? int.Parse(hexColor.Substring(6, 2), NumberStyles.HexNumber) : 255; + int r = int.Parse(hexColor[..2], NumberStyles.HexNumber); + int g = int.Parse(hexColor[2..4], NumberStyles.HexNumber); + int b = int.Parse(hexColor[4..6], NumberStyles.HexNumber); + int a = hexColor.Length == 8 ? int.Parse(hexColor[6..8], NumberStyles.HexNumber) : 255; return new Vector4(r / 255f, g / 255f, b / 255f, a / 255f); } diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index eb3acce..2b5431a 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -1,18 +1,21 @@ using Dalamud.Bindings.ImGui; +using Dalamud.Game.Gui.ContextMenu; +using Dalamud.Game.Text.SeStringHandling; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using System; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility; using LightlessSync.FileCache; using LightlessSync.Interop.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.Localization; @@ -24,8 +27,11 @@ using LightlessSync.Utils; using LightlessSync.WebAPI; using LightlessSync.WebAPI.SignalR; using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; @@ -70,7 +76,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase private bool _isOneDrive = false; private bool _isPenumbraDirectory = false; private bool _moodlesExists = false; - private Dictionary _oauthTokenExpiry = new(); + private readonly Dictionary _oauthTokenExpiry = []; private bool _penumbraExists = false; private bool _petNamesExists = false; private int _serverSelectionIndex = -1; @@ -178,15 +184,108 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase int i = 0; double dblSByte = bytes; - while (dblSByte >= 1000 && i < suffix.Length - 1) + while (dblSByte >= 1024 && i < suffix.Length - 1) { - dblSByte /= 1000.0; + dblSByte /= 1024.0; i++; } return addSuffix ? $"{dblSByte:0.00} {suffix[i]}" : $"{dblSByte:0.00}"; } + public readonly struct TabOption + { + public string Label { get; } + public T Value { get; } + public bool Enabled { get; } + + public TabOption(string label, T value, bool enabled = true) + { + Label = label; + Value = value; + Enabled = enabled; + } + } + + public static bool Tab(string id, IReadOnlyList> options, ref T selectedValue) where T : struct + { + if (options.Count == 0) + return false; + + var pushIdValue = string.IsNullOrEmpty(id) + ? $"UiSharedTab_{RuntimeHelpers.GetHashCode(options):X}" + : id; + using var tabId = ImRaii.PushId(pushIdValue); + + var selectedIndex = -1; + for (var i = 0; i < options.Count; i++) + { + if (!EqualityComparer.Default.Equals(options[i].Value, selectedValue)) + continue; + + selectedIndex = i; + break; + } + + if (selectedIndex == -1 || !options[selectedIndex].Enabled) + selectedIndex = GetFirstEnabledTabIndex(options); + + if (selectedIndex == -1) + return false; + + var changed = DrawTabsInternal(options, ref selectedIndex); + selectedValue = options[Math.Clamp(selectedIndex, 0, options.Count - 1)].Value; + return changed; + } + + private static int GetFirstEnabledTabIndex(IReadOnlyList> options) + { + for (var i = 0; i < options.Count; i++) + { + if (options[i].Enabled) + return i; + } + + return -1; + } + + private static bool DrawTabsInternal(IReadOnlyList> options, ref int selectedIndex) + { + selectedIndex = Math.Clamp(selectedIndex, 0, Math.Max(0, options.Count - 1)); + + var style = ImGui.GetStyle(); + var availableWidth = ImGui.GetContentRegionAvail().X; + var spacingX = style.ItemSpacing.X; + var buttonWidth = options.Count > 0 ? Math.Max(1f, (availableWidth - spacingX * (options.Count - 1)) / options.Count) : availableWidth; + var buttonHeight = Math.Max(ImGui.GetFrameHeight() + style.FramePadding.Y, 28f * ImGuiHelpers.GlobalScale); + var changed = false; + + for (var i = 0; i < options.Count; i++) + { + if (i > 0) + ImGui.SameLine(); + + var tab = options[i]; + var isSelected = i == selectedIndex; + + using (ImRaii.Disabled(!tab.Enabled)) + { + using var tabIndexId = ImRaii.PushId(i); + using var selectedButton = isSelected ? ImRaii.PushColor(ImGuiCol.Button, style.Colors[(int)ImGuiCol.TabActive]) : null; + using var selectedHover = isSelected ? ImRaii.PushColor(ImGuiCol.ButtonHovered, style.Colors[(int)ImGuiCol.TabHovered]) : null; + using var selectedActive = isSelected ? ImRaii.PushColor(ImGuiCol.ButtonActive, style.Colors[(int)ImGuiCol.TabActive]) : null; + + if (ImGui.Button(tab.Label, new Vector2(buttonWidth, buttonHeight))) + { + selectedIndex = i; + changed = true; + } + } + } + + return changed; + } + public static void CenterNextWindow(float width, float height, ImGuiCond cond = ImGuiCond.None) { var center = ImGui.GetMainViewport().GetCenter(); @@ -400,10 +499,21 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase public static bool ShiftPressed() => (GetKeyState(0xA1) & 0x8000) != 0 || (GetKeyState(0xA0) & 0x8000) != 0; - public static void TextWrapped(string text, float wrapPos = 0) + public static void TextWrapped(string text, float wrapPos = 0, Vector4? color = null) { ImGui.PushTextWrapPos(wrapPos); + if (color.HasValue) + { + ImGui.PushStyleColor(ImGuiCol.Text, color.Value); + } + ImGui.TextUnformatted(text); + + if (color.HasValue) + { + ImGui.PopStyleColor(); + } + ImGui.PopTextWrapPos(); } @@ -475,7 +585,22 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ); } - public void ColoredSeparator(Vector4? color = null, float thickness = 1f, float indent = 0f) + public static void AddContextMenuItem(IMenuOpenedArgs args, SeString name, char prefixChar, ushort colorMenuItem, Func onClick) + { + args.AddMenuItem(new MenuItem + { + Name = name, + PrefixChar = prefixChar, + UseDefaultPrefix = false, + PrefixColor = colorMenuItem, + OnClicked = _ => + { + onClick(); + }, + }); + } + + public static void ColoredSeparator(Vector4? color = null, float thickness = 1f, float indent = 0f) { var drawList = ImGui.GetWindowDrawList(); var min = ImGui.GetCursorScreenPos(); @@ -519,8 +644,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase bool changed = ImGui.Checkbox(label, ref value); + var boxSize = ImGui.GetFrameHeight(); var min = pos; - var max = ImGui.GetItemRectMax(); + var max = new Vector2(pos.X + boxSize, pos.Y + boxSize); var col = ImGui.GetColorU32(borderColor ?? ImGuiColors.DalamudGrey); ImGui.GetWindowDrawList().AddRect(min, max, col, rounding, ImDrawFlags.None, borderThickness); @@ -945,36 +1071,36 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.SameLine(150); ColorText("Penumbra", GetBoolColor(_penumbraExists)); - AttachToolTip($"Penumbra is " + (_penumbraExists ? "available and up to date." : "unavailable or not up to date.")); + AttachToolTip(BuildPluginTooltip("Penumbra", _penumbraExists, _ipcManager.Penumbra.State)); ImGui.SameLine(); ColorText("Glamourer", GetBoolColor(_glamourerExists)); - AttachToolTip($"Glamourer is " + (_glamourerExists ? "available and up to date." : "unavailable or not up to date.")); + AttachToolTip(BuildPluginTooltip("Glamourer", _glamourerExists, _ipcManager.Glamourer.State)); ImGui.TextUnformatted("Optional Plugins:"); ImGui.SameLine(150); ColorText("SimpleHeels", GetBoolColor(_heelsExists)); - AttachToolTip($"SimpleHeels is " + (_heelsExists ? "available and up to date." : "unavailable or not up to date.")); + AttachToolTip(BuildPluginTooltip("SimpleHeels", _heelsExists, _ipcManager.Heels.State)); ImGui.SameLine(); ColorText("Customize+", GetBoolColor(_customizePlusExists)); - AttachToolTip($"Customize+ is " + (_customizePlusExists ? "available and up to date." : "unavailable or not up to date.")); + AttachToolTip(BuildPluginTooltip("Customize+", _customizePlusExists, _ipcManager.CustomizePlus.State)); ImGui.SameLine(); ColorText("Honorific", GetBoolColor(_honorificExists)); - AttachToolTip($"Honorific is " + (_honorificExists ? "available and up to date." : "unavailable or not up to date.")); + AttachToolTip(BuildPluginTooltip("Honorific", _honorificExists, _ipcManager.Honorific.State)); ImGui.SameLine(); ColorText("Moodles", GetBoolColor(_moodlesExists)); - AttachToolTip($"Moodles is " + (_moodlesExists ? "available and up to date." : "unavailable or not up to date.")); + AttachToolTip(BuildPluginTooltip("Moodles", _moodlesExists, _ipcManager.Moodles.State)); ImGui.SameLine(); ColorText("PetNicknames", GetBoolColor(_petNamesExists)); - AttachToolTip($"PetNicknames is " + (_petNamesExists ? "available and up to date." : "unavailable or not up to date.")); + AttachToolTip(BuildPluginTooltip("PetNicknames", _petNamesExists, _ipcManager.PetNames.State)); ImGui.SameLine(); ColorText("Brio", GetBoolColor(_brioExists)); - AttachToolTip($"Brio is " + (_brioExists ? "available and up to date." : "unavailable or not up to date.")); + AttachToolTip(BuildPluginTooltip("Brio", _brioExists, _ipcManager.Brio.State)); if (!_penumbraExists || !_glamourerExists) { @@ -985,6 +1111,25 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase return true; } + private static string BuildPluginTooltip(string pluginName, bool isAvailable, IpcConnectionState state) + { + var availability = isAvailable ? "available and up to date." : "unavailable or not up to date."; + return $"{pluginName} is {availability}{Environment.NewLine}IPC State: {DescribeIpcState(state)}"; + } + + private static string DescribeIpcState(IpcConnectionState state) + => state switch + { + IpcConnectionState.Unknown => "Not evaluated yet", + IpcConnectionState.MissingPlugin => "Plugin not installed", + IpcConnectionState.VersionMismatch => "Installed version below required minimum", + IpcConnectionState.PluginDisabled => "Plugin installed but disabled", + IpcConnectionState.NotReady => "Plugin is not ready yet", + IpcConnectionState.Available => "Available", + IpcConnectionState.Error => "Error occurred while checking IPC", + _ => state.ToString() + }; + public int DrawServiceSelection(bool selectOnChange = false, bool showConnect = true) { string[] comboEntries = _serverConfigurationManager.GetServerNames(); @@ -1067,7 +1212,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase { using (ImRaii.Disabled(_discordOAuthUIDs == null)) { - var aliasPairs = _discordOAuthUIDs?.Result?.Select(t => new UIDAliasPair(t.Key, t.Value)).ToList() ?? [new UIDAliasPair(item.UID ?? null, null)]; + var aliasPairs = _discordOAuthUIDs?.Result?.Select(t => new UidAliasPair(t.Key, t.Value)).ToList() ?? [new UidAliasPair(item.UID ?? null, null)]; var uidComboName = "UID###" + item.CharacterName + item.WorldId + serverUri + indexOffset + aliasPairs.Count; DrawCombo(uidComboName, aliasPairs, (v) => @@ -1220,6 +1365,100 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase return _textureProvider.CreateFromImageAsync(imageData).Result; } + private static readonly (bool ItemHq, bool HiRes)[] IconLookupOrders = + [ + (false, true), + (true, true), + (false, false), + (true, false) + ]; + + public bool TryGetIcon(uint iconId, out IDalamudTextureWrap? wrap) + { + foreach (var (itemHq, hiRes) in IconLookupOrders) + { + if (TryGetIconWithLookup(iconId, itemHq, hiRes, out wrap)) + return true; + } + + foreach (var (itemHq, hiRes) in IconLookupOrders) + { + if (!_textureProvider.TryGetIconPath(new GameIconLookup(iconId, itemHq, hiRes), out var path) || string.IsNullOrEmpty(path)) + continue; + + try + { + var reference = _textureProvider.GetFromGame(path); + if (reference.TryGetWrap(out var texture, out _)) + { + wrap = texture; + return true; + } + } + catch (Exception ex) + { + Logger.LogTrace(ex, "Failed to load icon {IconId} from path {Path}", iconId, path); + } + } + + foreach (var hiRes in new[] { true, false }) + { + var manualPath = BuildIconPath(iconId, hiRes); + if (TryLoadTextureFromPath(manualPath, iconId, out wrap)) + return true; + } + + wrap = null; + return false; + } + + private bool TryLoadTextureFromPath(string path, uint iconId, out IDalamudTextureWrap? wrap) + { + try + { + var reference = _textureProvider.GetFromGame(path); + if (reference.TryGetWrap(out var texture, out _)) + { + wrap = texture; + return true; + } + } + catch (Exception ex) + { + Logger.LogTrace(ex, "Failed to load icon {IconId} from manual path {Path}", iconId, path); + } + + wrap = null; + return false; + } + + private static string BuildIconPath(uint iconId, bool hiRes) + { + var folder = iconId - iconId % 1000; + var basePath = $"ui/icon/{folder:000000}/{iconId:000000}"; + return hiRes ? $"{basePath}_hr1.tex" : $"{basePath}.tex"; + } + + private bool TryGetIconWithLookup(uint iconId, bool itemHq, bool hiRes, out IDalamudTextureWrap? wrap) + { + try + { + var icon = _textureProvider.GetFromGameIcon(new GameIconLookup(iconId, itemHq, hiRes)); + if (icon.TryGetWrap(out var texture, out _)) + { + wrap = texture; + return true; + } + } + catch (Exception ex) + { + Logger.LogTrace(ex, "Failed to load icon {IconId} (HQ:{ItemHq}, HR:{HiRes})", iconId, itemHq, hiRes); + } + + wrap = null; + return false; + } + public void LoadLocalization(string languageCode) { _localization.SetupWithLangCode(languageCode); @@ -1253,6 +1492,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase UidFont.Dispose(); GameFont.Dispose(); MediumFont.Dispose(); + _discordOAuthGetCts.Dispose(); } private static void CenterWindow(float width, float height, ImGuiCond cond = ImGuiCond.None) @@ -1285,13 +1525,24 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase num++; } - ImGui.PushID(text); + string displayText = text; + string idText = text; + int idSeparatorIndex = text.IndexOf("##", StringComparison.Ordinal); + if (idSeparatorIndex >= 0) + { + displayText = text[..idSeparatorIndex]; + idText = text[(idSeparatorIndex + 2)..]; + if (string.IsNullOrEmpty(idText)) + idText = displayText; + } + + ImGui.PushID(idText); Vector2 vector; using (IconFont.Push()) vector = ImGui.CalcTextSize(icon.ToIconString()); - Vector2 vector2 = ImGui.CalcTextSize(text); + Vector2 vector2 = ImGui.CalcTextSize(displayText); ImDrawListPtr windowDrawList = ImGui.GetWindowDrawList(); Vector2 cursorScreenPos = ImGui.GetCursorScreenPos(); float num2 = 3f * ImGuiHelpers.GlobalScale; @@ -1316,7 +1567,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase windowDrawList.AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString()); Vector2 pos2 = new Vector2(pos.X + vector.X + num2, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y); - windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), text); + windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), displayText); ImGui.PopID(); if (num > 0) { @@ -1325,6 +1576,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase return result; } + public sealed record IconScaleData(Vector2 IconSize, Vector2 NormalizedIconScale, float OffsetX, float IconScaling); - private record UIDAliasPair(string? UID, string? Alias); + private sealed record UidAliasPair(string? UID, string? Alias); } \ No newline at end of file diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index 02e0b4d..e1b3ab3 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -13,6 +13,8 @@ using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; using Dalamud.Interface; using LightlessSync.UI.Models; +using LightlessSync.UI.Style; +using LightlessSync.Utils; namespace LightlessSync.UI; @@ -25,39 +27,8 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase private ChangelogFile _changelog = new(); private CreditsFile _credits = new(); private bool _scrollToTop; - private int _selectedTab; private bool _hasInitializedCollapsingHeaders; - - private struct Particle - { - public Vector2 Position; - public Vector2 Velocity; - public float Life; - public float MaxLife; - public float Size; - public ParticleType Type; - public List? Trail; - public float Twinkle; - public float Depth; - public float Hue; - } - - private enum ParticleType - { - TwinklingStar, - ShootingStar - } - - private readonly List _particles = []; - private float _particleSpawnTimer; - private readonly Random _random = new(); - - private const float _headerHeight = 150f; - private const float _particleSpawnInterval = 0.2f; - private const int _maxParticles = 50; - private const int _maxTrailLength = 50; - private const float _edgeFadeDistance = 30f; - private const float _extendedParticleHeight = 40f; + private readonly AnimatedHeader _animatedHeader = new(); public UpdateNotesUi(ILogger logger, LightlessMediator mediator, @@ -70,21 +41,20 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase _uiShared = uiShared; _configService = configService; - AllowClickthrough = false; - AllowPinning = false; RespectCloseHotkey = true; ShowCloseButton = true; Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove; - SizeConstraints = new WindowSizeConstraints() - { - MinimumSize = new Vector2(800, 700), MaximumSize = new Vector2(800, 700), - }; - PositionCondition = ImGuiCond.Always; + WindowBuilder.For(this) + .AllowPinning(false) + .AllowClickthrough(false) + .SetFixedSize(new Vector2(800, 700)) + .Apply(); + LoadEmbeddedResources(); logger.LogInformation("UpdateNotesUi constructor completed successfully"); } @@ -95,6 +65,11 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase _hasInitializedCollapsingHeaders = false; } + public override void OnClose() + { + _animatedHeader.ClearParticles(); + } + private void CenterWindow() { var viewport = ImGui.GetMainViewport(); @@ -117,21 +92,18 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase private void DrawHeader() { - var windowPos = ImGui.GetWindowPos(); var windowPadding = ImGui.GetStyle().WindowPadding; var headerWidth = (800f * ImGuiHelpers.GlobalScale) - (windowPadding.X * 2); - var headerStart = windowPos + new Vector2(windowPadding.X, windowPadding.Y); - var headerEnd = headerStart + new Vector2(headerWidth, _headerHeight); + var buttons = new List + { + new(FontAwesomeIcon.Comments, "Join our Discord", () => Util.OpenLink("https://discord.gg/dsbjcXMnhA")), + new(FontAwesomeIcon.Code, "View on Git", () => Util.OpenLink("https://git.lightless-sync.org/Lightless-Sync")) + }; - var extendedParticleSize = new Vector2(headerWidth, _headerHeight + _extendedParticleHeight); + _animatedHeader.DrawWithButtons(headerWidth, "Lightless Sync", "Update Notes", buttons, _uiShared.UidFont); - DrawGradientBackground(headerStart, headerEnd); - DrawHeaderText(headerStart); - DrawHeaderButtons(headerStart, headerWidth); - DrawBottomGradient(headerStart, headerEnd, headerWidth); - - ImGui.SetCursorPosY(windowPadding.Y + _headerHeight + 5); + ImGui.SetCursorPosY(windowPadding.Y + _animatedHeader.Height + 5); ImGui.SetCursorPosX(20); using (ImRaii.PushFont(UiBuilder.IconFont)) { @@ -156,347 +128,8 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase } ImGuiHelpers.ScaledDummy(3); - - DrawParticleEffects(headerStart, extendedParticleSize); } - private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd) - { - var drawList = ImGui.GetWindowDrawList(); - - var darkPurple = new Vector4(0.08f, 0.05f, 0.15f, 1.0f); - var deepPurple = new Vector4(0.12f, 0.08f, 0.20f, 1.0f); - - drawList.AddRectFilledMultiColor( - headerStart, - headerEnd, - ImGui.GetColorU32(darkPurple), - ImGui.GetColorU32(darkPurple), - ImGui.GetColorU32(deepPurple), - ImGui.GetColorU32(deepPurple) - ); - - var random = new Random(42); - for (int i = 0; i < 50; i++) - { - var starPos = headerStart + new Vector2( - (float)random.NextDouble() * (headerEnd.X - headerStart.X), - (float)random.NextDouble() * (headerEnd.Y - headerStart.Y) - ); - var brightness = 0.3f + (float)random.NextDouble() * 0.4f; - drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(new Vector4(1f, 1f, 1f, brightness))); - } - } - - private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width) - { - var drawList = ImGui.GetWindowDrawList(); - var gradientHeight = 60f; - - for (int i = 0; i < gradientHeight; i++) - { - var progress = i / gradientHeight; - var smoothProgress = progress * progress; - var r = 0.12f + (0.0f - 0.12f) * smoothProgress; - var g = 0.08f + (0.0f - 0.08f) * smoothProgress; - var b = 0.20f + (0.0f - 0.20f) * smoothProgress; - var alpha = 1f - smoothProgress; - var gradientColor = new Vector4(r, g, b, alpha); - drawList.AddLine( - new Vector2(headerStart.X, headerEnd.Y + i), - new Vector2(headerStart.X + width, headerEnd.Y + i), - ImGui.GetColorU32(gradientColor), - 1f - ); - } - } - - private void DrawHeaderText(Vector2 headerStart) - { - var textX = 20f; - var textY = 30f; - - ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY)); - - using (_uiShared.UidFont.Push()) - { - ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), "Lightless Sync"); - } - - ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f)); - ImGui.TextColored(UIColors.Get("LightlessBlue"), "Update Notes"); - } - - private void DrawHeaderButtons(Vector2 headerStart, float headerWidth) - { - var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Globe); - var spacing = 8f * ImGuiHelpers.GlobalScale; - var rightPadding = 15f * ImGuiHelpers.GlobalScale; - var topPadding = 15f * ImGuiHelpers.GlobalScale; - var buttonY = headerStart.Y + topPadding; - var gitButtonX = headerStart.X + headerWidth - rightPadding - buttonSize.X; - var discordButtonX = gitButtonX - buttonSize.X - spacing; - - ImGui.SetCursorScreenPos(new Vector2(discordButtonX, buttonY)); - - using (ImRaii.PushColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0))) - using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple") with { W = 0.3f })) - using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive") with { W = 0.5f })) - { - if (_uiShared.IconButton(FontAwesomeIcon.Comments)) - { - Util.OpenLink("https://discord.gg/dsbjcXMnhA"); - } - - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("Join our Discord"); - } - - ImGui.SetCursorScreenPos(new Vector2(gitButtonX, buttonY)); - if (_uiShared.IconButton(FontAwesomeIcon.Code)) - { - Util.OpenLink("https://git.lightless-sync.org/Lightless-Sync"); - } - - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("View on Git"); - } - } - } - - private void DrawParticleEffects(Vector2 bannerStart, Vector2 bannerSize) - { - var deltaTime = ImGui.GetIO().DeltaTime; - _particleSpawnTimer += deltaTime; - - if (_particleSpawnTimer > _particleSpawnInterval && _particles.Count < _maxParticles) - { - SpawnParticle(bannerSize); - _particleSpawnTimer = 0f; - } - - if (_random.NextDouble() < 0.003) - { - SpawnShootingStar(bannerSize); - } - - var drawList = ImGui.GetWindowDrawList(); - - for (int i = _particles.Count - 1; i >= 0; i--) - { - var particle = _particles[i]; - - var screenPos = bannerStart + particle.Position; - - if (particle.Type == ParticleType.ShootingStar && particle.Trail != null) - { - particle.Trail.Insert(0, particle.Position); - if (particle.Trail.Count > _maxTrailLength) - particle.Trail.RemoveAt(particle.Trail.Count - 1); - } - - if (particle.Type == ParticleType.TwinklingStar) - { - particle.Twinkle += 0.005f * particle.Depth; - } - - particle.Position += particle.Velocity * deltaTime; - particle.Life -= deltaTime; - - var isOutOfBounds = particle.Position.X < -50 || particle.Position.X > bannerSize.X + 50 || - particle.Position.Y < -50 || particle.Position.Y > bannerSize.Y + 50; - - if (particle.Life <= 0 || (particle.Type != ParticleType.TwinklingStar && isOutOfBounds)) - { - _particles.RemoveAt(i); - continue; - } - - if (particle.Type == ParticleType.TwinklingStar) - { - if (particle.Position.X < 0 || particle.Position.X > bannerSize.X) - particle.Velocity = particle.Velocity with { X = -particle.Velocity.X }; - if (particle.Position.Y < 0 || particle.Position.Y > bannerSize.Y) - particle.Velocity = particle.Velocity with { Y = -particle.Velocity.Y }; - } - - var fadeIn = Math.Min(1f, (particle.MaxLife - particle.Life) / 20f); - var fadeOut = Math.Min(1f, particle.Life / 20f); - var lifeFade = Math.Min(fadeIn, fadeOut); - - var edgeFadeX = Math.Min( - Math.Min(1f, (particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance), - Math.Min(1f, (bannerSize.X - particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance) - ); - var edgeFadeY = Math.Min( - Math.Min(1f, (particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance), - Math.Min(1f, (bannerSize.Y - particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance) - ); - var edgeFade = Math.Min(edgeFadeX, edgeFadeY); - - var baseAlpha = lifeFade * edgeFade; - var finalAlpha = particle.Type == ParticleType.TwinklingStar - ? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle)) - : baseAlpha; - - if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1) - { - var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f); - - for (int t = 1; t < particle.Trail.Count; t++) - { - var trailProgress = (float)t / particle.Trail.Count; - var trailAlpha = Math.Min(1f, (1f - trailProgress) * finalAlpha * 1.8f); - var trailWidth = (1f - trailProgress) * 3f + 1f; - - var glowAlpha = trailAlpha * 0.4f; - drawList.AddLine( - bannerStart + particle.Trail[t - 1], - bannerStart + particle.Trail[t], - ImGui.GetColorU32(cyanColor with { W = glowAlpha }), - trailWidth + 4f - ); - - drawList.AddLine( - bannerStart + particle.Trail[t - 1], - bannerStart + particle.Trail[t], - ImGui.GetColorU32(cyanColor with { W = trailAlpha }), - trailWidth - ); - } - } - else if (particle.Type == ParticleType.TwinklingStar) - { - DrawTwinklingStar(drawList, screenPos, particle.Size, particle.Hue, finalAlpha, particle.Depth); - } - - _particles[i] = particle; - } - } - - private void DrawTwinklingStar(ImDrawListPtr drawList, Vector2 position, float size, float hue, float alpha, - float depth) - { - var color = HslToRgb(hue, 1.0f, 0.85f); - color.W = alpha; - - drawList.AddCircleFilled(position, size, ImGui.GetColorU32(color)); - - var glowColor = color with { W = alpha * 0.3f }; - drawList.AddCircleFilled(position, size * (1.2f + depth * 0.3f), ImGui.GetColorU32(glowColor)); - } - - private static Vector4 HslToRgb(float h, float s, float l) - { - h = h / 360f; - float c = (1 - MathF.Abs(2 * l - 1)) * s; - float x = c * (1 - MathF.Abs((h * 6) % 2 - 1)); - float m = l - c / 2; - - float r, g, b; - if (h < 1f / 6f) - { - r = c; - g = x; - b = 0; - } - else if (h < 2f / 6f) - { - r = x; - g = c; - b = 0; - } - else if (h < 3f / 6f) - { - r = 0; - g = c; - b = x; - } - else if (h < 4f / 6f) - { - r = 0; - g = x; - b = c; - } - else if (h < 5f / 6f) - { - r = x; - g = 0; - b = c; - } - else - { - r = c; - g = 0; - b = x; - } - - return new Vector4(r + m, g + m, b + m, 1.0f); - } - - - private void SpawnParticle(Vector2 bannerSize) - { - var position = new Vector2( - (float)_random.NextDouble() * bannerSize.X, - (float)_random.NextDouble() * bannerSize.Y - ); - - var depthLayers = new[] { 0.5f, 1.0f, 1.5f }; - var depth = depthLayers[_random.Next(depthLayers.Length)]; - - var velocity = new Vector2( - ((float)_random.NextDouble() - 0.5f) * 0.05f * depth, - ((float)_random.NextDouble() - 0.5f) * 0.05f * depth - ); - - var isBlue = _random.NextDouble() < 0.5; - var hue = isBlue ? 220f + (float)_random.NextDouble() * 30f : 270f + (float)_random.NextDouble() * 40f; - var size = (0.5f + (float)_random.NextDouble() * 2f) * depth; - var maxLife = 120f + (float)_random.NextDouble() * 60f; - - _particles.Add(new Particle - { - Position = position, - Velocity = velocity, - Life = maxLife, - MaxLife = maxLife, - Size = size, - Type = ParticleType.TwinklingStar, - Trail = null, - Twinkle = (float)_random.NextDouble() * MathF.PI * 2, - Depth = depth, - Hue = hue - }); - } - - private void SpawnShootingStar(Vector2 bannerSize) - { - var maxLife = 80f + (float)_random.NextDouble() * 40f; - var startX = bannerSize.X * (0.3f + (float)_random.NextDouble() * 0.6f); - var startY = -10f; - - _particles.Add(new Particle - { - Position = new Vector2(startX, startY), - Velocity = new Vector2( - -50f - (float)_random.NextDouble() * 40f, - 30f + (float)_random.NextDouble() * 40f - ), - Life = maxLife, - MaxLife = maxLife, - Size = 2.5f, - Type = ParticleType.ShootingStar, - Trail = new List(), - Twinkle = 0, - Depth = 1.0f, - Hue = 270f - }); - } - - private void DrawTabs() { using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f)) @@ -513,7 +146,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { if (changelogTab) { - _selectedTab = 0; DrawChangelog(); } } @@ -524,7 +156,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { if (creditsTab) { - _selectedTab = 1; DrawCredits(); } } @@ -558,19 +189,21 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase } } - private void DrawCreditCategory(CreditCategory category) + private static void DrawCreditCategory(CreditCategory category) { DrawFeatureSection(category.Category, UIColors.Get("LightlessBlue")); foreach (var item in category.Items) { + ImGui.Bullet(); + ImGui.SameLine(); if (!string.IsNullOrEmpty(item.Role)) { - ImGui.BulletText($"{item.Name} — {item.Role}"); + ImGui.TextWrapped($"{item.Name} — {item.Role}"); } else { - ImGui.BulletText(item.Name); + ImGui.TextWrapped(item.Name); } } @@ -623,7 +256,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase ImGui.SetScrollHereY(0); } - ImGui.PushTextWrapPos(); + ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X); foreach (var entry in _changelog.Changelog) { @@ -683,7 +316,9 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase foreach (var item in version.Items) { - ImGui.BulletText(item); + ImGui.Bullet(); + ImGui.SameLine(); + ImGui.TextWrapped(item); } ImGuiHelpers.ScaledDummy(5); @@ -745,7 +380,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase using var changelogStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.changelog.yaml"); if (changelogStream != null) { - using var reader = new StreamReader(changelogStream, Encoding.UTF8, true, 128); + using var reader = new StreamReader(changelogStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 128); var yaml = reader.ReadToEnd(); _changelog = deserializer.Deserialize(yaml) ?? new(); } @@ -754,7 +389,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase using var creditsStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.credits.yaml"); if (creditsStream != null) { - using var reader = new StreamReader(creditsStream, Encoding.UTF8, true, 128); + using var reader = new StreamReader(creditsStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 128); var yaml = reader.ReadToEnd(); _credits = deserializer.Deserialize(yaml) ?? new(); } diff --git a/LightlessSync/UI/ZoneChatUi.cs b/LightlessSync/UI/ZoneChatUi.cs new file mode 100644 index 0000000..396e63c --- /dev/null +++ b/LightlessSync/UI/ZoneChatUi.cs @@ -0,0 +1,2020 @@ +using System.Globalization; +using System.Numerics; +using LightlessSync.API.Data; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using LightlessSync.API.Dto.Chat; +using LightlessSync.LightlessConfiguration; +using LightlessSync.LightlessConfiguration.Models; +using LightlessSync.Services; +using LightlessSync.Services.Chat; +using LightlessSync.Services.LightFinder; +using LightlessSync.Services.Mediator; +using LightlessSync.Services.ServerConfiguration; +using LightlessSync.UI.Services; +using LightlessSync.UI.Style; +using LightlessSync.Utils; +using LightlessSync.WebAPI; +using LightlessSync.WebAPI.SignalR.Utils; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.UI; + +public sealed class ZoneChatUi : WindowMediatorSubscriberBase +{ + private const string ChatDisabledStatus = "Chat services disabled"; + private const string ZoneUnavailableStatus = "Zone chat is only available in major cities."; + private const string SettingsPopupId = "zone_chat_settings_popup"; + private const string ReportPopupId = "Report Message##zone_chat_report_popup"; + private const string ChannelDragPayloadId = "zone_chat_channel_drag"; + private const float DefaultWindowOpacity = .97f; + private const float DefaultUnfocusedWindowOpacity = 0.6f; + private const float MinWindowOpacity = 0.05f; + private const float MaxWindowOpacity = 1f; + private const float MinChatFontScale = 0.75f; + private const float MaxChatFontScale = 1.5f; + private const float UnfocusedFadeOutSpeed = 0.22f; + private const float FocusFadeInSpeed = 2.0f; + private const int ReportReasonMaxLength = 500; + private const int ReportContextMaxLength = 1000; + private const int MaxChannelNoteTabLength = 25; + + private readonly UiSharedService _uiSharedService; + private readonly ZoneChatService _zoneChatService; + private readonly PairUiService _pairUiService; + private readonly LightFinderService _lightFinderService; + private readonly LightlessProfileManager _profileManager; + private readonly ApiController _apiController; + private readonly ChatConfigService _chatConfigService; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly DalamudUtilService _dalamudUtilService; + private readonly IUiBuilder _uiBuilder; + private readonly Dictionary _draftMessages = new(StringComparer.Ordinal); + private readonly ImGuiWindowFlags _unpinnedWindowFlags; + private float _currentWindowOpacity = DefaultWindowOpacity; + private float _baseWindowOpacity = DefaultWindowOpacity; + private bool _isWindowPinned; + private bool _showRulesOverlay; + private bool _refocusChatInput; + private string? _refocusChatInputKey; + private bool _isWindowFocused = true; + private int _titleBarStylePopCount; + + private string? _selectedChannelKey; + private bool _scrollToBottom = true; + private float? _pendingChannelScroll; + private float _channelScroll; + private float _channelScrollMax; + private readonly SeluneBrush _seluneBrush = new(); + private ChatChannelSnapshot? _reportTargetChannel; + private ChatMessageEntry? _reportTargetMessage; + private string _reportReason = string.Empty; + private string _reportAdditionalContext = string.Empty; + private bool _reportPopupOpen; + private bool _reportPopupRequested; + private bool _reportSubmitting; + private string? _reportError; + private ChatReportResult? _reportSubmissionResult; + private string? _dragChannelKey; + private string? _dragHoverKey; + private bool _HideStateActive; + private bool _HideStateWasOpen; + + public ZoneChatUi( + ILogger logger, + LightlessMediator mediator, + UiSharedService uiSharedService, + ZoneChatService zoneChatService, + PairUiService pairUiService, + LightFinderService lightFinderService, + LightlessProfileManager profileManager, + ChatConfigService chatConfigService, + ServerConfigurationManager serverConfigurationManager, + DalamudUtilService dalamudUtilService, + IUiBuilder uiBuilder, + ApiController apiController, + PerformanceCollectorService performanceCollectorService) + : base(logger, mediator, "Lightless Chat", performanceCollectorService) + { + _uiSharedService = uiSharedService; + _zoneChatService = zoneChatService; + _pairUiService = pairUiService; + _lightFinderService = lightFinderService; + _profileManager = profileManager; + _chatConfigService = chatConfigService; + _serverConfigurationManager = serverConfigurationManager; + _dalamudUtilService = dalamudUtilService; + _uiBuilder = uiBuilder; + _apiController = apiController; + _isWindowPinned = _chatConfigService.Current.IsWindowPinned; + _showRulesOverlay = _chatConfigService.Current.ShowRulesOverlayOnOpen; + if (_chatConfigService.Current.AutoOpenChatOnPluginLoad) + { + IsOpen = true; + } + _unpinnedWindowFlags = Flags; + RefreshWindowFlags(); + ApplyUiVisibilitySettings(); + Size = new Vector2(450, 420) * ImGuiHelpers.GlobalScale; + SizeCondition = ImGuiCond.FirstUseEver; + WindowBuilder.For(this) + .SetSizeConstraints( + new Vector2(320f, 260f) * ImGuiHelpers.GlobalScale, + new Vector2(900f, 900f) * ImGuiHelpers.GlobalScale) + .Apply(); + + Mediator.Subscribe(this, OnChatChannelMessageAdded); + Mediator.Subscribe(this, _ => _scrollToBottom = true); + Mediator.Subscribe(this, _ => UpdateHideState()); + Mediator.Subscribe(this, _ => UpdateHideState()); + } + + public override void PreDraw() + { + RefreshWindowFlags(); + base.PreDraw(); + var config = _chatConfigService.Current; + var baseOpacity = Math.Clamp(config.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity); + _baseWindowOpacity = baseOpacity; + + if (config.FadeWhenUnfocused) + { + var unfocusedOpacity = Math.Clamp(config.UnfocusedWindowOpacity, MinWindowOpacity, MaxWindowOpacity); + var targetOpacity = _isWindowFocused ? baseOpacity : Math.Min(baseOpacity, unfocusedOpacity); + var delta = ImGui.GetIO().DeltaTime; + var speed = _isWindowFocused ? FocusFadeInSpeed : UnfocusedFadeOutSpeed; + _currentWindowOpacity = MoveTowards(_currentWindowOpacity, targetOpacity, speed * delta); + } + else + { + _currentWindowOpacity = baseOpacity; + } + + ImGui.SetNextWindowBgAlpha(_currentWindowOpacity); + PushTitleBarFadeColors(_currentWindowOpacity); + } + + private void UpdateHideState() + { + ApplyUiVisibilitySettings(); + var shouldHide = ShouldHide(); + if (shouldHide) + { + _HideStateWasOpen |= IsOpen; + if (IsOpen) + { + IsOpen = false; + } + _HideStateActive = true; + } + else if (_HideStateActive) + { + if (_HideStateWasOpen) + { + IsOpen = true; + } + _HideStateActive = false; + _HideStateWasOpen = false; + } + } + + private void ApplyUiVisibilitySettings() + { + var config = _chatConfigService.Current; + _uiBuilder.DisableAutomaticUiHide = config.ShowWhenUiHidden; + _uiBuilder.DisableCutsceneUiHide = config.ShowInCutscenes; + _uiBuilder.DisableGposeUiHide = config.ShowInGpose; + } + + private bool ShouldHide() + { + var config = _chatConfigService.Current; + + if (config.HideInCombat && _dalamudUtilService.IsInCombat) + { + return true; + } + + if (config.HideInDuty && _dalamudUtilService.IsInDuty && !_dalamudUtilService.IsInFieldOperation) + { + return true; + } + + return false; + } + + protected override void DrawInternal() + { + if (_titleBarStylePopCount > 0) + { + ImGui.PopStyleColor(_titleBarStylePopCount); + _titleBarStylePopCount = 0; + } + + var config = _chatConfigService.Current; + var isFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows); + var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows); + if (config.FadeWhenUnfocused && isHovered && !isFocused) + { + ImGui.SetWindowFocus(); + } + + _isWindowFocused = config.FadeWhenUnfocused ? (isFocused || isHovered) : isFocused; + + var contentAlpha = 1f; + if (config.FadeWhenUnfocused) + { + var baseOpacity = MathF.Max(_baseWindowOpacity, 0.001f); + contentAlpha = Math.Clamp(_currentWindowOpacity / baseOpacity, 0f, 1f); + } + + using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, contentAlpha); + var drawList = ImGui.GetWindowDrawList(); + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize); + var childBgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.ChildBg]; + childBgColor.W *= _baseWindowOpacity; + using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, childBgColor); + DrawConnectionControls(); + + var channels = _zoneChatService.GetChannelsSnapshot(); + DrawReportPopup(); + + if (channels.Count == 0) + { + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); + ImGui.TextWrapped("No chat channels available."); + ImGui.PopStyleColor(); + } + else + { + EnsureSelectedChannel(channels); + CleanupDrafts(channels); + + DrawChannelButtons(channels); + + if (_selectedChannelKey is null) + { + selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime); + return; + } + + var activeChannel = channels.FirstOrDefault(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal)); + if (activeChannel.Equals(default(ChatChannelSnapshot))) + { + activeChannel = channels[0]; + _selectedChannelKey = activeChannel.Key; + } + + _zoneChatService.SetActiveChannel(activeChannel.Key); + + DrawHeader(activeChannel); + ImGui.Separator(); + DrawMessageArea(activeChannel, _currentWindowOpacity); + ImGui.Separator(); + DrawInput(activeChannel); + } + + if (_showRulesOverlay) + { + DrawRulesOverlay(); + } + + selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime); + } + + private void PushTitleBarFadeColors(float opacity) + { + _titleBarStylePopCount = 0; + var alpha = Math.Clamp(opacity, 0f, 1f); + var colors = ImGui.GetStyle().Colors; + + var titleBg = colors[(int)ImGuiCol.TitleBg]; + var titleBgActive = colors[(int)ImGuiCol.TitleBgActive]; + var titleBgCollapsed = colors[(int)ImGuiCol.TitleBgCollapsed]; + + ImGui.PushStyleColor(ImGuiCol.TitleBg, new Vector4(titleBg.X, titleBg.Y, titleBg.Z, titleBg.W * alpha)); + ImGui.PushStyleColor(ImGuiCol.TitleBgActive, new Vector4(titleBgActive.X, titleBgActive.Y, titleBgActive.Z, titleBgActive.W * alpha)); + ImGui.PushStyleColor(ImGuiCol.TitleBgCollapsed, new Vector4(titleBgCollapsed.X, titleBgCollapsed.Y, titleBgCollapsed.Z, titleBgCollapsed.W * alpha)); + _titleBarStylePopCount = 3; + } + + private void DrawHeader(ChatChannelSnapshot channel) + { + var prefix = channel.Type == ChatChannelType.Zone ? "Zone" : "Syncshell"; + Vector4 color; + + if (!channel.IsConnected) + { + color = UIColors.Get("DimRed"); + } + else if (!channel.IsAvailable) + { + color = ImGuiColors.DalamudGrey3; + } + else + { + color = channel.Type == ChatChannelType.Zone ? UIColors.Get("LightlessPurple") : UIColors.Get("LightlessBlue"); + } + + ImGui.TextColored(color, $"{prefix}: {channel.DisplayName}"); + + if (channel.Type == ChatChannelType.Zone && channel.Descriptor.WorldId != 0) + { + ImGui.SameLine(); + var worldId = channel.Descriptor.WorldId; + var worldName = _dalamudUtilService.WorldData.Value.TryGetValue(worldId, out var name) ? name : $"World #{worldId}"; + ImGui.TextUnformatted(worldName); + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip($"World ID: {worldId}"); + } + } + + var showInlineStatus = string.Equals(channel.StatusText, ChatDisabledStatus, StringComparison.OrdinalIgnoreCase) + || string.Equals(channel.StatusText, ZoneUnavailableStatus, StringComparison.OrdinalIgnoreCase); + if (showInlineStatus) + { + ImGui.SameLine(); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); + ImGui.TextUnformatted($"| {channel.StatusText}"); + ImGui.PopStyleColor(); + } + else if (!string.IsNullOrEmpty(channel.StatusText)) + { + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); + ImGui.TextWrapped(channel.StatusText); + ImGui.PopStyleColor(); + } + } + + private void DrawMessageArea(ChatChannelSnapshot channel, float windowOpacity) + { + var available = ImGui.GetContentRegionAvail(); + var inputHeight = ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y; + var height = Math.Max(100f * ImGuiHelpers.GlobalScale, available.Y - inputHeight); + + using var child = ImRaii.Child($"chat_messages_{channel.Key}", new Vector2(-1, height), false); + if (!child) + return; + + var configuredFontScale = Math.Clamp(_chatConfigService.Current.ChatFontScale, MinChatFontScale, MaxChatFontScale); + var restoreFontScale = false; + if (Math.Abs(configuredFontScale - 1f) > 0.001f) + { + ImGui.SetWindowFontScale(configuredFontScale); + restoreFontScale = true; + } + + var drawList = ImGui.GetWindowDrawList(); + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + var gradientBottom = UIColors.Get("LightlessPurple"); + var bottomAlpha = 0.12f * windowOpacity; + var bottomColorVec = new Vector4(gradientBottom.X, gradientBottom.Y, gradientBottom.Z, bottomAlpha); + var topColorVec = new Vector4(gradientBottom.X, gradientBottom.Y, gradientBottom.Z, 0.0f); + var bottomColor = ImGui.ColorConvertFloat4ToU32(bottomColorVec); + var topColor = ImGui.ColorConvertFloat4ToU32(topColorVec); + drawList.AddRectFilledMultiColor( + windowPos, + windowPos + windowSize, + topColor, + topColor, + bottomColor, + bottomColor); + + var showTimestamps = _chatConfigService.Current.ShowMessageTimestamps; + + if (channel.Messages.Count == 0) + { + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); + ImGui.TextWrapped("Chat history will appear here when available."); + ImGui.PopStyleColor(); + } + else + { + for (var i = 0; i < channel.Messages.Count; i++) + { + var message = channel.Messages[i]; + ImGui.PushID(i); + + if (message.IsSystem) + { + DrawSystemEntry(message); + ImGui.PopID(); + continue; + } + + if (message.Payload is not { } payload) + { + ImGui.PopID(); + continue; + } + + var timestampText = string.Empty; + if (showTimestamps) + { + timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] "; + } + var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite; + + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {payload.Message}"); + ImGui.PopStyleColor(); + + if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}")) + { + var contextLocalTimestamp = payload.SentAtUtc.ToLocalTime(); + var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture); + ImGui.TextDisabled(contextTimestampText); + ImGui.Separator(); + + var actionIndex = 0; + foreach (var action in GetContextMenuActions(channel, message)) + { + DrawContextMenuAction(action, actionIndex++); + } + + ImGui.EndPopup(); + } + + ImGui.PopID(); + } + } + + if (_scrollToBottom) + { + ImGui.SetScrollHereY(1f); + _scrollToBottom = false; + } + + if (restoreFontScale) + { + ImGui.SetWindowFontScale(1f); + } + } + + private void DrawInput(ChatChannelSnapshot channel) + { + const int MaxMessageLength = ZoneChatService.MaxOutgoingLength; + var canSend = channel.IsConnected && channel.IsAvailable; + _draftMessages.TryGetValue(channel.Key, out var draft); + draft ??= string.Empty; + + var style = ImGui.GetStyle(); + var sendButtonWidth = 100f * ImGuiHelpers.GlobalScale; + var counterWidth = ImGui.CalcTextSize($"{MaxMessageLength}/{MaxMessageLength}").X; + var reservedWidth = sendButtonWidth + counterWidth + style.ItemSpacing.X * 2f; + + ImGui.SetNextItemWidth(-reservedWidth); + var inputId = $"##chat-input-{channel.Key}"; + if (_refocusChatInput && string.Equals(_refocusChatInputKey, channel.Key, StringComparison.Ordinal)) + { + ImGui.SetKeyboardFocusHere(); + _refocusChatInput = false; + _refocusChatInputKey = null; + } + ImGui.InputText(inputId, ref draft, MaxMessageLength); + if (ImGui.IsItemActive() || ImGui.IsItemFocused()) + { + var drawList = ImGui.GetWindowDrawList(); + var itemMin = ImGui.GetItemRectMin(); + var itemMax = ImGui.GetItemRectMax(); + var highlight = UIColors.Get("LightlessPurple").WithAlpha(0.35f); + var highlightU32 = ImGui.ColorConvertFloat4ToU32(highlight); + drawList.AddRect(itemMin, itemMax, highlightU32, style.FrameRounding, ImDrawFlags.None, Math.Max(1f, ImGuiHelpers.GlobalScale)); + } + var enterPressed = ImGui.IsItemFocused() + && (ImGui.IsKeyPressed(ImGuiKey.Enter) || ImGui.IsKeyPressed(ImGuiKey.KeypadEnter)); + _draftMessages[channel.Key] = draft; + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); + ImGui.TextUnformatted($"{draft.Length}/{MaxMessageLength}"); + ImGui.PopStyleColor(); + + ImGui.SameLine(); + var buttonScreenPos = ImGui.GetCursorScreenPos(); + var rightEdgeScreen = ImGui.GetWindowPos().X + ImGui.GetWindowContentRegionMax().X; + var desiredButtonX = rightEdgeScreen - sendButtonWidth; + var minButtonX = buttonScreenPos.X + style.ItemSpacing.X; + var finalButtonX = MathF.Max(minButtonX, desiredButtonX); + ImGui.SetCursorScreenPos(new Vector2(finalButtonX, buttonScreenPos.Y)); + var sendColor = UIColors.Get("LightlessPurpleDefault"); + var sendHovered = UIColors.Get("LightlessPurple"); + var sendActive = UIColors.Get("LightlessPurpleActive"); + ImGui.PushStyleColor(ImGuiCol.Button, sendColor); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, sendHovered); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, sendActive); + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 6f * ImGuiHelpers.GlobalScale); + var sendClicked = false; + using (ImRaii.Disabled(!canSend)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PaperPlane, $"Send##chat-send-{channel.Key}", 100f * ImGuiHelpers.GlobalScale, center: true)) + { + sendClicked = true; + } + } + ImGui.PopStyleVar(); + ImGui.PopStyleColor(3); + + if (canSend && (enterPressed || sendClicked)) + { + _refocusChatInput = true; + _refocusChatInputKey = channel.Key; + if (TrySendDraft(channel, draft)) + { + _draftMessages[channel.Key] = string.Empty; + _scrollToBottom = true; + } + } + } + + private void DrawRulesOverlay() + { + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + var parentContentMin = ImGui.GetWindowContentRegionMin(); + var parentContentMax = ImGui.GetWindowContentRegionMax(); + var overlayPos = windowPos + parentContentMin; + var overlaySize = parentContentMax - parentContentMin; + + if (overlaySize.X <= 0f || overlaySize.Y <= 0f) + { + overlayPos = windowPos; + overlaySize = windowSize; + } + + ImGui.SetNextWindowFocus(); + ImGui.SetNextWindowPos(overlayPos); + ImGui.SetNextWindowSize(overlaySize); + ImGui.SetNextWindowBgAlpha(0.86f); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 6f * ImGuiHelpers.GlobalScale); + ImGui.PushStyleColor(ImGuiCol.Border, Vector4.Zero); + + var overlayFlags = ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.NoMove + | ImGuiWindowFlags.NoScrollbar + | ImGuiWindowFlags.NoSavedSettings; + + var overlayOpen = true; + if (ImGui.Begin("##zone_chat_rules_overlay", ref overlayOpen, overlayFlags)) + { + var contentMin = ImGui.GetWindowContentRegionMin(); + var contentMax = ImGui.GetWindowContentRegionMax(); + var contentWidth = contentMax.X - contentMin.X; + var title = "Chat Rules"; + var titleWidth = ImGui.CalcTextSize(title).X; + + ImGui.SetCursorPosX(contentMin.X + Math.Max(0f, (contentWidth - titleWidth) * 0.5f)); + ImGui.TextUnformatted(title); + + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + var style = ImGui.GetStyle(); + var buttonWidth = 180f * ImGuiHelpers.GlobalScale; + var buttonHeight = ImGui.GetFrameHeight(); + var buttonSpacing = Math.Max(0f, style.ItemSpacing.Y); + var buttonTopY = Math.Max(contentMin.Y, contentMax.Y - buttonHeight); + var childHeight = Math.Max(0f, buttonTopY - buttonSpacing - ImGui.GetCursorPosY()); + + using (var child = ImRaii.Child("zone_chat_rules_overlay_scroll", new Vector2(-1f, childHeight), false)) + { + if (child) + { + var childContentMin = ImGui.GetWindowContentRegionMin(); + var childContentMax = ImGui.GetWindowContentRegionMax(); + var childContentWidth = childContentMax.X - childContentMin.X; + + ImGui.PushTextWrapPos(childContentMin.X + childContentWidth); + + _uiSharedService.MediumText("Basic Chat Rules", UIColors.Get("LightlessBlue")); + + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("Do "), + new SeStringUtils.RichTextEntry("NOT share", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(" confidential, personal, or account information-yours or anyone else's", UIColors.Get("LightlessYellow"), true), + new SeStringUtils.RichTextEntry(".")); + + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("Respect ALL participants; "), + new SeStringUtils.RichTextEntry("no harassment, hate speech, or personal attacks", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(".")); + + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("AVOID ", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry("disruptive behaviors such as "), + new SeStringUtils.RichTextEntry("spamming or flooding", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(".")); + + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("Absolutely "), + new SeStringUtils.RichTextEntry("NO discussion, sharing, or solicitation of illegal content or activities;", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(".")); + + ImGui.Dummy(new Vector2(5)); + + ImGui.Separator(); + _uiSharedService.MediumText("Zone Chat Rules", UIColors.Get("LightlessGreen")); + + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("NO ADVERTISEMENTS", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(" whatsoever for any kind of "), + new SeStringUtils.RichTextEntry("services, venues, clubs, marketboard deals, or similar offerings", UIColors.Get("LightlessYellow"), true), + new SeStringUtils.RichTextEntry(".")); + + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), + new SeStringUtils.RichTextEntry("Mature", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(" or "), + new SeStringUtils.RichTextEntry("NSFW", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry(" content "), + new SeStringUtils.RichTextEntry("(including suggestive emotes, explicit innuendo, or roleplay (including requests) )", UIColors.Get("LightlessYellow"), true), + new SeStringUtils.RichTextEntry(" is"), + new SeStringUtils.RichTextEntry(" strictly prohibited ", UIColors.Get("DimRed"), true), + new SeStringUtils.RichTextEntry("in Zone Chat.")); + + ImGui.Dummy(new Vector2(5)); + + ImGui.Separator(); + _uiSharedService.MediumText("Syncshell Chat Rules", UIColors.Get("LightlessYellow")); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("Syncshell chats are self-moderated (their own set rules) by it's owner and appointed moderators.")); + + ImGui.Dummy(new Vector2(5)); + + ImGui.Separator(); + _uiSharedService.MediumText("Reporting & Punishments", UIColors.Get("LightlessBlue")); + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"), + new SeStringUtils.RichTextEntry("Report rule-breakers by right clicking on the sent message and clicking report or via the mod-mail channel on the Discord."), + new SeStringUtils.RichTextEntry(" (False reports may be punished.) ", UIColors.Get("DimRed"), true)); + + _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"), + new SeStringUtils.RichTextEntry("Punishments scale from a permanent chat ban up to a full Lightless account ban."), + new SeStringUtils.RichTextEntry(" (Appeals are possible, but will be accepted only in clear cases of error.) ", UIColors.Get("DimRed"), true)); + + ImGui.PopTextWrapPos(); + } + } + + var spacingStartY = Math.Max(ImGui.GetCursorPosY(), buttonTopY - buttonSpacing); + ImGui.SetCursorPosY(spacingStartY); + + if (buttonSpacing > 0f) + { + var actualSpacing = Math.Max(0f, buttonTopY - spacingStartY); + if (actualSpacing > 0f) + { + ImGui.Dummy(new Vector2(0f, actualSpacing)); + } + } + + ImGui.SetCursorPosY(buttonTopY); + + var buttonX = contentMin.X + Math.Max(0f, (contentWidth - buttonWidth) * 0.5f); + ImGui.SetCursorPosX(buttonX); + + if (ImGui.Button("I Understand", new Vector2(buttonWidth, buttonHeight))) + { + _showRulesOverlay = false; + } + + if (!overlayOpen) + { + _showRulesOverlay = false; + } + } + + ImGui.End(); + ImGui.PopStyleColor(); + ImGui.PopStyleVar(); + } + + private void DrawReportPopup() + { + if (!_reportPopupOpen) + return; + + var desiredPopupSize = new Vector2(520f * ImGuiHelpers.GlobalScale, 0f); + ImGui.SetNextWindowSize(desiredPopupSize, ImGuiCond.Always); + if (_reportPopupRequested) + { + ImGui.OpenPopup(ReportPopupId); + _reportPopupRequested = false; + } + else if (!ImGui.IsPopupOpen(ReportPopupId, ImGuiPopupFlags.AnyPopupLevel)) + { + ImGui.OpenPopup(ReportPopupId); + } + + var popupFlags = UiSharedService.PopupWindowFlags | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings; + if (!ImGui.BeginPopupModal(ReportPopupId, popupFlags)) + return; + + if (_reportTargetChannel is not { } channel || _reportTargetMessage is not { } message) + { + CloseReportPopup(); + ImGui.EndPopup(); + return; + } + + if (message.Payload is not { } payload) + { + CloseReportPopup(); + ImGui.EndPopup(); + return; + } + + if (_reportSubmissionResult is { } pendingResult) + { + _reportSubmissionResult = null; + _reportSubmitting = false; + + if (pendingResult.Success) + { + Mediator.Publish(new NotificationMessage("Zone Chat", "Report submitted for moderator review.", NotificationType.Info, TimeSpan.FromSeconds(3))); + CloseReportPopup(); + ImGui.EndPopup(); + return; + } + + _reportError = pendingResult.ErrorMessage ?? "Failed to submit report. Please try again."; + } + + var channelPrefix = channel.Type == ChatChannelType.Zone ? "Zone" : "Syncshell"; + var channelLabel = $"{channelPrefix}: {channel.DisplayName}"; + if (channel.Type == ChatChannelType.Zone && channel.Descriptor.WorldId != 0) + { + channelLabel += $" (World #{channel.Descriptor.WorldId})"; + } + + ImGui.TextUnformatted(channelLabel); + ImGui.TextUnformatted($"Sender: {message.DisplayName}"); + ImGui.TextUnformatted($"Sent: {payload.SentAtUtc.ToLocalTime().ToString("g", CultureInfo.CurrentCulture)}"); + + ImGui.Separator(); + ImGui.PushTextWrapPos(ImGui.GetWindowContentRegionMax().X); + ImGui.TextWrapped(payload.Message); + ImGui.PopTextWrapPos(); + ImGui.Separator(); + + ImGui.TextUnformatted("Reason (required)"); + if (ImGui.InputTextMultiline("##chat_report_reason", ref _reportReason, ReportReasonMaxLength, new Vector2(-1, 80f * ImGuiHelpers.GlobalScale)) + && _reportReason.Length > ReportReasonMaxLength) + { + _reportReason = _reportReason[..ReportReasonMaxLength]; + } + + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); + ImGui.TextUnformatted($"{_reportReason.Length}/{ReportReasonMaxLength}"); + ImGui.PopStyleColor(); + + ImGui.Spacing(); + ImGui.TextUnformatted("Additional context (optional)"); + if (ImGui.InputTextMultiline("##chat_report_context", ref _reportAdditionalContext, ReportContextMaxLength, new Vector2(-1, 120f * ImGuiHelpers.GlobalScale)) + && _reportAdditionalContext.Length > ReportContextMaxLength) + { + _reportAdditionalContext = _reportAdditionalContext[..ReportContextMaxLength]; + } + + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); + ImGui.TextUnformatted($"{_reportAdditionalContext.Length}/{ReportContextMaxLength}"); + ImGui.PopStyleColor(); + + if (!string.IsNullOrEmpty(_reportError)) + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.TextWrapped(_reportError); + ImGui.PopStyleColor(); + } + + if (_reportSubmitting) + { + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); + ImGui.TextUnformatted("Submitting report..."); + ImGui.PopStyleColor(); + } + + ImGui.Separator(); + var style = ImGui.GetStyle(); + var availableWidth = Math.Max(0f, ImGui.GetContentRegionAvail().X); + var buttonWidth = Math.Max(100f * ImGuiHelpers.GlobalScale, (availableWidth - style.ItemSpacing.X) / 2f); + var canSubmit = !_reportSubmitting && _reportReason.Trim().Length > 0; + + using (ImRaii.Disabled(!canSubmit)) + { + if (ImGui.Button("Submit Report", new Vector2(buttonWidth, 0f)) && canSubmit) + { + BeginReportSubmission(channel, message); + } + } + + ImGui.SameLine(); + if (ImGui.Button("Cancel", new Vector2(buttonWidth, 0f))) + { + CloseReportPopup(); + } + + ImGui.EndPopup(); + } + + private void OpenReportPopup(ChatChannelSnapshot channel, ChatMessageEntry message) + { + if (message.Payload is not { } payload) + { + _logger.LogDebug("Ignoring report popup request for non-message entry in {ChannelKey}", channel.Key); + return; + } + + _reportTargetChannel = channel; + _reportTargetMessage = message; + _logger.LogDebug("Opening report popup for channel {ChannelKey}, message {MessageId}", channel.Key, payload.MessageId); + _reportReason = string.Empty; + _reportAdditionalContext = string.Empty; + _reportError = null; + _reportSubmissionResult = null; + _reportSubmitting = false; + _reportPopupOpen = true; + _reportPopupRequested = true; + } + + private void BeginReportSubmission(ChatChannelSnapshot channel, ChatMessageEntry message) + { + if (_reportSubmitting) + return; + + if (message.Payload is not { } payload) + { + _reportError = "Unable to report this message."; + return; + } + + var trimmedReason = _reportReason.Trim(); + if (trimmedReason.Length == 0) + { + _reportError = "Please describe the issue before submitting."; + return; + } + + var trimmedContext = string.IsNullOrWhiteSpace(_reportAdditionalContext) + ? null + : _reportAdditionalContext.Trim(); + + _reportSubmitting = true; + _reportError = null; + _reportSubmissionResult = null; + + var descriptor = channel.Descriptor; + var messageId = payload.MessageId; + if (string.IsNullOrWhiteSpace(messageId)) + { + _reportSubmitting = false; + _reportError = "Unable to report this message."; + _logger.LogWarning("Report submission aborted: missing message id for channel {ChannelKey}", channel.Key); + return; + } + + _logger.LogDebug("Submitting chat report for channel {ChannelKey}, message {MessageId}", channel.Key, messageId); + _ = Task.Run(async () => + { + ChatReportResult result; + try + { + result = await _zoneChatService.ReportMessageAsync(descriptor, messageId, trimmedReason, trimmedContext).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to submit chat report"); + result = new ChatReportResult(false, "Failed to submit report. Please try again."); + } + + _reportSubmissionResult = result; + if (result.Success) + { + _logger.LogInformation("Chat report submitted successfully for channel {ChannelKey}, message {MessageId}", channel.Key, messageId); + } + else + { + _logger.LogWarning("Chat report submission failed for channel {ChannelKey}, message {MessageId}: {Error}", channel.Key, messageId, result.ErrorMessage); + } + }); + } + + private void CloseReportPopup() + { + _reportPopupOpen = false; + _reportPopupRequested = false; + ResetReportPopupState(); + ImGui.CloseCurrentPopup(); + } + + private void ResetReportPopupState() + { + _reportTargetChannel = null; + _reportTargetMessage = null; + _reportReason = string.Empty; + _reportAdditionalContext = string.Empty; + _reportError = null; + _reportSubmissionResult = null; + _reportSubmitting = false; + _reportPopupRequested = false; + } + + private bool TrySendDraft(ChatChannelSnapshot channel, string draft) + { + var trimmed = draft.Trim(); + if (trimmed.Length == 0) + return false; + + bool succeeded; + try + { + succeeded = _zoneChatService.SendMessageAsync(channel.Descriptor, trimmed).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to send chat message"); + succeeded = false; + } + + return succeeded; + } + + private IEnumerable GetContextMenuActions(ChatChannelSnapshot channel, ChatMessageEntry message) + { + if (message.IsSystem || message.Payload is not { } payload) + yield break; + + if (TryCreateCopyMessageAction(message, payload, out var copyAction)) + { + yield return copyAction; + } + + if (TryCreateViewProfileAction(channel, message, payload, out var viewProfile)) + { + yield return viewProfile; + } + + if (TryCreateMuteParticipantAction(channel, message, payload, out var muteAction)) + { + yield return muteAction; + } + + if (TryCreateReportMessageAction(channel, message, payload, out var reportAction)) + { + yield return reportAction; + } + } + + private static bool TryCreateCopyMessageAction(ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action) + { + var text = payload.Message; + if (string.IsNullOrEmpty(text)) + { + action = default; + return false; + } + + action = new ChatMessageContextAction( + FontAwesomeIcon.Clipboard, + "Copy Message", + true, + () => ImGui.SetClipboardText(text)); + return true; + } + + private bool TryCreateViewProfileAction(ChatChannelSnapshot channel, ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action) + { + action = default; + switch (channel.Type) + { + case ChatChannelType.Group: + { + var user = payload.Sender.User; + if (user?.UID is not { Length: > 0 }) + return false; + + var snapshot = _pairUiService.GetSnapshot(); + if (snapshot.PairsByUid.TryGetValue(user.UID, out var pair) && pair is not null) + { + action = new ChatMessageContextAction( + FontAwesomeIcon.User, + "View Profile", + true, + () => Mediator.Publish(new ProfileOpenStandaloneMessage(pair))); + return true; + } + + action = new ChatMessageContextAction( + FontAwesomeIcon.User, + "View Profile", + true, + () => RunContextAction(() => OpenStandardProfileAsync(user))); + return true; + + } + case ChatChannelType.Zone: + if (!payload.Sender.CanResolveProfile) + return false; + + var hashedCid = payload.Sender.HashedCid; + if (string.IsNullOrEmpty(hashedCid)) + return false; + + action = new ChatMessageContextAction( + FontAwesomeIcon.User, + "View Profile", + true, + () => RunContextAction(() => OpenLightfinderProfileInternalAsync(hashedCid))); + return true; + default: + return false; + } + } + + private bool TryCreateMuteParticipantAction(ChatChannelSnapshot channel, ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action) + { + action = default; + if (message.FromSelf) + return false; + + if (string.IsNullOrEmpty(payload.Sender.Token)) + return false; + + var safeName = string.IsNullOrWhiteSpace(message.DisplayName) + ? "Participant" + : message.DisplayName; + + action = new ChatMessageContextAction( + FontAwesomeIcon.VolumeMute, + $"Mute '{safeName}'", + true, + () => RunContextAction(() => _zoneChatService.SetParticipantMuteAsync(channel.Descriptor, payload.Sender.Token!, true))); + return true; + } + + private bool TryCreateReportMessageAction(ChatChannelSnapshot channel, ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action) + { + action = default; + if (message.FromSelf) + return false; + + if (string.IsNullOrWhiteSpace(payload.MessageId)) + return false; + + action = new ChatMessageContextAction( + FontAwesomeIcon.ExclamationTriangle, + "Report Message", + true, + () => OpenReportPopup(channel, message)); + + return true; + } + + private Task OpenStandardProfileAsync(UserData user) + { + _profileManager.GetLightlessProfile(user); + _profileManager.GetLightlessUserProfile(user); + Mediator.Publish(new OpenUserProfileMessage(user)); + return Task.CompletedTask; + } + + private void RunContextAction(Func action) + { + _ = Task.Run(async () => + { + try + { + await action().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Chat context action failed"); + Mediator.Publish(new NotificationMessage("Zone Chat", "Action failed to complete.", NotificationType.Error, TimeSpan.FromSeconds(3))); + } + }); + } + + private void OnChatChannelMessageAdded(ChatChannelMessageAdded message) + { + if (_selectedChannelKey is not null && string.Equals(_selectedChannelKey, message.ChannelKey, StringComparison.Ordinal)) + { + _scrollToBottom = true; + } + } + + private async Task OpenLightfinderProfileInternalAsync(string hashedCid) + { + var profile = await _profileManager.GetLightfinderProfileAsync(hashedCid).ConfigureAwait(false); + if (profile is null) + { + Mediator.Publish(new NotificationMessage("Zone Chat", "Unable to load Lightfinder profile information.", NotificationType.Warning, TimeSpan.FromSeconds(3))); + return; + } + + var sanitizedUser = profile.Value.User with + { + UID = "Lightfinder User", + Alias = "Lightfinder User" + }; + + Mediator.Publish(new OpenLightfinderProfileMessage(sanitizedUser, profile.Value.ProfileData, hashedCid)); + } + + private void EnsureSelectedChannel(IReadOnlyList channels) + { + if (_selectedChannelKey is not null && channels.Any(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal))) + return; + + _selectedChannelKey = channels.Count > 0 ? channels[0].Key : null; + if (_selectedChannelKey is not null) + { + _zoneChatService.SetActiveChannel(_selectedChannelKey); + _scrollToBottom = true; + } + } + + private void CleanupDrafts(IReadOnlyList channels) + { + var existingKeys = new HashSet(channels.Select(c => c.Key), StringComparer.Ordinal); + foreach (var key in _draftMessages.Keys.ToList()) + { + if (!existingKeys.Contains(key)) + { + _draftMessages.Remove(key); + } + } + + if (_refocusChatInputKey is not null && !existingKeys.Contains(_refocusChatInputKey)) + { + _refocusChatInputKey = null; + _refocusChatInput = false; + } + } + + private void DrawConnectionControls() + { + var hubState = _apiController.ServerState; + var chatEnabled = _zoneChatService.IsChatEnabled; + var chatConnected = _zoneChatService.IsChatConnected; + var buttonLabel = chatEnabled ? "Disable Chat" : "Enable Chat"; + var style = ImGui.GetStyle(); + var cursorStart = ImGui.GetCursorPos(); + var contentRightX = cursorStart.X + ImGui.GetContentRegionAvail().X; + var rulesButtonWidth = 90f * ImGuiHelpers.GlobalScale; + + using (ImRaii.Group()) + { + if (ImGui.Button(buttonLabel, new Vector2(130f * ImGuiHelpers.GlobalScale, 0f))) + { + ToggleChatConnection(chatEnabled); + } + + ImGui.SameLine(); + var chatStatusText = chatEnabled + ? (chatConnected ? "Chat: Connected" : "Chat: Waiting") + : "Chat: Disabled"; + var statusColor = chatEnabled + ? (chatConnected ? UIColors.Get("LightlessGreen") : UIColors.Get("LightlessYellow")) + : ImGuiColors.DalamudGrey3; + ImGui.PushStyleColor(ImGuiCol.Text, statusColor); + ImGui.TextUnformatted(chatStatusText); + ImGui.PopStyleColor(); + + if (!string.IsNullOrWhiteSpace(_apiController.AuthFailureMessage) && hubState == ServerState.Unauthorized) + { + ImGui.SameLine(); + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); + ImGui.TextUnformatted(_apiController.AuthFailureMessage); + ImGui.PopStyleColor(); + } + } + + var groupSize = ImGui.GetItemRectSize(); + var minBlockX = cursorStart.X + groupSize.X + style.ItemSpacing.X; + var availableAfterGroup = contentRightX - (cursorStart.X + groupSize.X); + var lightfinderButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.PersonCirclePlus).X; + var settingsButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog).X; + var pinIcon = _isWindowPinned ? FontAwesomeIcon.Lock : FontAwesomeIcon.Unlock; + var pinButtonWidth = _uiSharedService.GetIconButtonSize(pinIcon).X; + var blockWidth = lightfinderButtonWidth + style.ItemSpacing.X + rulesButtonWidth + style.ItemSpacing.X + settingsButtonWidth + style.ItemSpacing.X + pinButtonWidth; + var desiredBlockX = availableAfterGroup > blockWidth + style.ItemSpacing.X + ? contentRightX - blockWidth + : minBlockX; + desiredBlockX = Math.Max(cursorStart.X, desiredBlockX); + var lightfinderPos = new Vector2(desiredBlockX, cursorStart.Y); + var rulesPos = new Vector2(lightfinderPos.X + lightfinderButtonWidth + style.ItemSpacing.X, cursorStart.Y); + var settingsPos = new Vector2(rulesPos.X + rulesButtonWidth + style.ItemSpacing.X, cursorStart.Y); + var pinPos = new Vector2(settingsPos.X + settingsButtonWidth + style.ItemSpacing.X, cursorStart.Y); + + ImGui.SameLine(); + ImGui.SetCursorPos(lightfinderPos); + var lightfinderEnabled = _lightFinderService.IsBroadcasting; + var lightfinderColor = lightfinderEnabled ? UIColors.Get("LightlessGreen") : ImGuiColors.DalamudGrey3; + var lightfinderButtonSize = new Vector2(lightfinderButtonWidth, ImGui.GetFrameHeight()); + ImGui.InvisibleButton("zone_chat_lightfinder_button", lightfinderButtonSize); + var lightfinderMin = ImGui.GetItemRectMin(); + var lightfinderMax = ImGui.GetItemRectMax(); + var iconSize = _uiSharedService.GetIconSize(FontAwesomeIcon.PersonCirclePlus); + var iconPos = new Vector2( + lightfinderMin.X + (lightfinderButtonSize.X - iconSize.X) * 0.5f, + lightfinderMin.Y + (lightfinderButtonSize.Y - iconSize.Y) * 0.5f); + using (_uiSharedService.IconFont.Push()) + { + ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(lightfinderColor), FontAwesomeIcon.PersonCirclePlus.ToIconString()); + } + + if (ImGui.IsItemClicked()) + { + Mediator.Publish(new UiToggleMessage(typeof(LightFinderUI))); + } + if (ImGui.IsItemHovered()) + { + var padding = new Vector2(8f * ImGuiHelpers.GlobalScale); + Selune.RegisterHighlight( + lightfinderMin - padding, + lightfinderMax + padding, + SeluneHighlightMode.Point, + exactSize: true, + clipToElement: true, + clipPadding: padding, + highlightColorOverride: lightfinderColor, + highlightAlphaOverride: 0.2f); + ImGui.SetTooltip("If Lightfinder is enabled, you will be able to see the character names of other Lightfinder users in the same zone when they send a message."); + } + + ImGui.SameLine(); + ImGui.SetCursorPos(rulesPos); + if (ImGui.Button("Rules", new Vector2(rulesButtonWidth, 0f))) + { + _showRulesOverlay = true; + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Show chat rules"); + } + + ImGui.SameLine(); + ImGui.SetCursorPos(settingsPos); + if (_uiSharedService.IconButton(FontAwesomeIcon.Cog)) + { + ImGui.OpenPopup(SettingsPopupId); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Chat settings"); + } + + ImGui.SameLine(); + ImGui.SetCursorPos(pinPos); + using (ImRaii.PushId("window_pin_button")) + { + var restorePinColors = false; + if (_isWindowPinned) + { + var pinBase = UIColors.Get("LightlessPurpleDefault"); + var pinHover = UIColors.Get("LightlessPurple").WithAlpha(0.9f); + var pinActive = UIColors.Get("LightlessPurpleActive"); + ImGui.PushStyleColor(ImGuiCol.Button, pinBase); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, pinHover); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, pinActive); + restorePinColors = true; + } + + var pinClicked = _uiSharedService.IconButton(pinIcon); + + if (restorePinColors) + { + ImGui.PopStyleColor(3); + } + + if (pinClicked) + { + ToggleWindowPinned(); + } + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(_isWindowPinned ? "Unpin window" : "Pin window"); + } + + DrawChatSettingsPopup(); + + ImGui.Separator(); + } + + private void ToggleWindowPinned() + { + _isWindowPinned = !_isWindowPinned; + _chatConfigService.Current.IsWindowPinned = _isWindowPinned; + _chatConfigService.Save(); + RefreshWindowFlags(); + } + + private void RefreshWindowFlags() + { + Flags = _unpinnedWindowFlags & ~ImGuiWindowFlags.NoCollapse; + if (_isWindowPinned) + { + Flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; + } + } + + private void DrawChatSettingsPopup() + { + const ImGuiWindowFlags popupFlags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoSavedSettings; + if (!ImGui.BeginPopup(SettingsPopupId, popupFlags)) + return; + + ImGui.TextUnformatted("Chat Settings"); + ImGui.Separator(); + + var chatConfig = _chatConfigService.Current; + + var autoEnable = chatConfig.AutoEnableChatOnLogin; + if (ImGui.Checkbox("Auto-enable chat on login", ref autoEnable)) + { + chatConfig.AutoEnableChatOnLogin = autoEnable; + _chatConfigService.Save(); + if (autoEnable && !_zoneChatService.IsChatEnabled) + { + ToggleChatConnection(currentlyEnabled: false); + } + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Automatically connect to chat whenever Lightless loads."); + } + + var autoOpen = chatConfig.AutoOpenChatOnPluginLoad; + if (ImGui.Checkbox("Auto-open chat window on plugin load", ref autoOpen)) + { + chatConfig.AutoOpenChatOnPluginLoad = autoOpen; + _chatConfigService.Save(); + if (autoOpen) + { + IsOpen = true; + } + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Opens the chat window automatically whenever the plugin loads."); + } + + var showRules = chatConfig.ShowRulesOverlayOnOpen; + if (ImGui.Checkbox("Show rules overlay on open", ref showRules)) + { + chatConfig.ShowRulesOverlayOnOpen = showRules; + _chatConfigService.Save(); + _showRulesOverlay = showRules; + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Toggles if the rules popup appears everytime the chat is opened for the first time."); + } + + var showTimestamps = chatConfig.ShowMessageTimestamps; + if (ImGui.Checkbox("Show message timestamps", ref showTimestamps)) + { + chatConfig.ShowMessageTimestamps = showTimestamps; + _chatConfigService.Save(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Toggles the timestamp prefix on messages."); + } + + ImGui.Separator(); + ImGui.TextUnformatted("Chat Visibility"); + + var autoHideCombat = chatConfig.HideInCombat; + if (ImGui.Checkbox("Hide in combat", ref autoHideCombat)) + { + chatConfig.HideInCombat = autoHideCombat; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Temporarily hides the chat window while in combat."); + } + + var autoHideDuty = chatConfig.HideInDuty; + if (ImGui.Checkbox("Hide in duty (Not in field operations)", ref autoHideDuty)) + { + chatConfig.HideInDuty = autoHideDuty; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Hides the chat window inside duties."); + } + + var showWhenUiHidden = chatConfig.ShowWhenUiHidden; + if (ImGui.Checkbox("Show when game UI is hidden", ref showWhenUiHidden)) + { + chatConfig.ShowWhenUiHidden = showWhenUiHidden; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Allow the chat window to remain visible when the game UI is hidden."); + } + + var showInCutscenes = chatConfig.ShowInCutscenes; + if (ImGui.Checkbox("Show in cutscenes", ref showInCutscenes)) + { + chatConfig.ShowInCutscenes = showInCutscenes; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Allow the chat window to remain visible during cutscenes."); + } + + var showInGpose = chatConfig.ShowInGpose; + if (ImGui.Checkbox("Show in group pose", ref showInGpose)) + { + chatConfig.ShowInGpose = showInGpose; + _chatConfigService.Save(); + UpdateHideState(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Allow the chat window to remain visible in /gpose."); + } + + ImGui.Separator(); + + var fontScale = Math.Clamp(chatConfig.ChatFontScale, MinChatFontScale, MaxChatFontScale); + var fontScaleChanged = ImGui.SliderFloat("Message font scale", ref fontScale, MinChatFontScale, MaxChatFontScale, "%.2fx"); + var resetFontScale = ImGui.IsItemClicked(ImGuiMouseButton.Right); + if (resetFontScale) + { + fontScale = 1.0f; + fontScaleChanged = true; + } + + if (fontScaleChanged) + { + chatConfig.ChatFontScale = fontScale; + _chatConfigService.Save(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Adjust scale of chat message text.\nRight-click to reset to default."); + } + + var windowOpacity = Math.Clamp(chatConfig.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity); + var opacityChanged = ImGui.SliderFloat("Window transparency", ref windowOpacity, MinWindowOpacity, MaxWindowOpacity, "%.2f"); + var resetOpacity = ImGui.IsItemClicked(ImGuiMouseButton.Right); + if (resetOpacity) + { + windowOpacity = DefaultWindowOpacity; + opacityChanged = true; + } + + if (opacityChanged) + { + chatConfig.ChatWindowOpacity = windowOpacity; + _chatConfigService.Save(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Adjust chat window transparency.\nRight-click to reset to default."); + } + + var fadeUnfocused = chatConfig.FadeWhenUnfocused; + if (ImGui.Checkbox("Fade window when unfocused", ref fadeUnfocused)) + { + chatConfig.FadeWhenUnfocused = fadeUnfocused; + _chatConfigService.Save(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("When enabled, the chat window fades after it loses focus.\nHovering the window restores focus."); + } + + ImGui.BeginDisabled(!fadeUnfocused); + var unfocusedOpacity = Math.Clamp(chatConfig.UnfocusedWindowOpacity, MinWindowOpacity, MaxWindowOpacity); + var unfocusedChanged = ImGui.SliderFloat("Unfocused transparency", ref unfocusedOpacity, MinWindowOpacity, MaxWindowOpacity, "%.2f"); + var resetUnfocused = ImGui.IsItemClicked(ImGuiMouseButton.Right); + if (resetUnfocused) + { + unfocusedOpacity = DefaultUnfocusedWindowOpacity; + unfocusedChanged = true; + } + if (unfocusedChanged) + { + chatConfig.UnfocusedWindowOpacity = unfocusedOpacity; + _chatConfigService.Save(); + } + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Target transparency while the chat window is unfocused.\nRight-click to reset to default."); + } + ImGui.EndDisabled(); + + ImGui.EndPopup(); + } + + private static float MoveTowards(float current, float target, float maxDelta) + { + if (current < target) + { + return MathF.Min(current + maxDelta, target); + } + + if (current > target) + { + return MathF.Max(current - maxDelta, target); + } + + return target; + } + + private void ToggleChatConnection(bool currentlyEnabled) + { + _ = Task.Run(async () => + { + try + { + await _zoneChatService.SetChatEnabledAsync(!currentlyEnabled).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to toggle chat connection"); + } + }); + } + + private unsafe void DrawChannelButtons(IReadOnlyList channels) + { + var style = ImGui.GetStyle(); + var baseFramePadding = style.FramePadding; + var available = ImGui.GetContentRegionAvail().X; + var buttonHeight = ImGui.GetFrameHeight(); + var arrowWidth = buttonHeight; + var hasChannels = channels.Count > 0; + var scrollWidth = hasChannels ? Math.Max(0f, available - (arrowWidth * 2f + style.ItemSpacing.X * 2f)) : 0f; + if (hasChannels) + { + var minimumWidth = 120f * ImGuiHelpers.GlobalScale; + scrollWidth = Math.Max(scrollWidth, minimumWidth); + } + var scrollStep = scrollWidth > 0f ? scrollWidth * 0.9f : 120f; + if (!hasChannels) + { + _pendingChannelScroll = null; + _channelScroll = 0f; + _channelScrollMax = 0f; + } + var prevScroll = hasChannels ? _channelScroll : 0f; + var prevMax = hasChannels ? _channelScrollMax : 0f; + float currentScroll = prevScroll; + float maxScroll = prevMax; + + ImGui.PushID("chat_channel_buttons"); + ImGui.BeginGroup(); + + using (ImRaii.Disabled(!hasChannels || prevScroll <= 0.5f)) + { + var arrowNormal = UIColors.Get("ButtonDefault"); + var arrowHovered = UIColors.Get("LightlessPurple").WithAlpha(0.85f); + var arrowActive = UIColors.Get("LightlessPurpleDefault").WithAlpha(0.75f); + ImGui.PushStyleColor(ImGuiCol.Button, arrowNormal); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, arrowHovered); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, arrowActive); + var clickedLeft = ImGui.ArrowButton("##chat_left", ImGuiDir.Left); + ImGui.PopStyleColor(3); + if (clickedLeft) + { + _pendingChannelScroll = Math.Max(0f, currentScroll - scrollStep); + } + } + + ImGui.SameLine(0f, style.ItemSpacing.X); + + var childHeight = buttonHeight + style.FramePadding.Y * 2f + style.ScrollbarSize; + var alignPushed = false; + if (hasChannels) + { + ImGui.PushStyleVar(ImGuiStyleVar.ButtonTextAlign, new Vector2(0f, 0.5f)); + alignPushed = true; + } + + const int MaxBadgeDisplay = 99; + + using (var child = ImRaii.Child("channel_scroll", new Vector2(scrollWidth, childHeight), false, ImGuiWindowFlags.HorizontalScrollbar)) + { + if (child) + { + var dragActive = _dragChannelKey is not null && ImGui.IsMouseDragging(ImGuiMouseButton.Left); + var hoveredTargetThisFrame = false; + var first = true; + foreach (var channel in channels) + { + if (!first) + ImGui.SameLine(); + + var isSelected = string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal); + var showBadge = !isSelected && channel.UnreadCount > 0; + var isZoneChannel = channel.Type == ChatChannelType.Zone; + (string Text, Vector2 TextSize, float Width, float Height)? badgeMetrics = null; + var channelLabel = GetChannelTabLabel(channel); + + var normal = isSelected ? UIColors.Get("LightlessPurpleDefault") : UIColors.Get("ButtonDefault"); + var hovered = isSelected + ? UIColors.Get("LightlessPurple").WithAlpha(0.9f) + : UIColors.Get("ButtonDefault").WithAlpha(0.85f); + var active = isSelected + ? UIColors.Get("LightlessPurpleDefault").WithAlpha(0.8f) + : UIColors.Get("ButtonDefault").WithAlpha(0.75f); + + ImGui.PushStyleColor(ImGuiCol.Button, normal); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hovered); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, active); + + if (showBadge) + { + var badgeSpacing = 4f * ImGuiHelpers.GlobalScale; + var badgePadding = new Vector2(4f, 1.5f) * ImGuiHelpers.GlobalScale; + var badgeText = channel.UnreadCount > MaxBadgeDisplay + ? $"{MaxBadgeDisplay}+" + : channel.UnreadCount.ToString(CultureInfo.InvariantCulture); + var badgeTextSize = ImGui.CalcTextSize(badgeText); + var badgeWidth = badgeTextSize.X + badgePadding.X * 2f; + var badgeHeight = badgeTextSize.Y + badgePadding.Y * 2f; + var customPadding = new Vector2(baseFramePadding.X + badgeWidth + badgeSpacing, baseFramePadding.Y); + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, customPadding); + badgeMetrics = (badgeText, badgeTextSize, badgeWidth, badgeHeight); + } + + var clicked = ImGui.Button($"{channelLabel}##chat_channel_{channel.Key}"); + + if (showBadge) + { + ImGui.PopStyleVar(); + } + + ImGui.PopStyleColor(3); + + if (clicked && !isSelected) + { + _selectedChannelKey = channel.Key; + _zoneChatService.SetActiveChannel(channel.Key); + _scrollToBottom = true; + } + + if (ShouldShowChannelTabContextMenu(channel) + && ImGui.BeginPopupContextItem($"chat_channel_ctx##{channel.Key}")) + { + DrawChannelTabContextMenu(channel); + ImGui.EndPopup(); + } + + if (ImGui.BeginDragDropSource(ImGuiDragDropFlags.None)) + { + if (!string.Equals(_dragChannelKey, channel.Key, StringComparison.Ordinal)) + { + _dragHoverKey = null; + } + + _dragChannelKey = channel.Key; + ImGui.SetDragDropPayload(ChannelDragPayloadId, null, 0); + ImGui.TextUnformatted(channelLabel); + ImGui.EndDragDropSource(); + } + + var isDragTarget = false; + + if (ImGui.BeginDragDropTarget()) + { + var acceptFlags = ImGuiDragDropFlags.AcceptBeforeDelivery | ImGuiDragDropFlags.AcceptNoDrawDefaultRect; + var payload = ImGui.AcceptDragDropPayload(ChannelDragPayloadId, acceptFlags); + if (!payload.IsNull && _dragChannelKey is { } draggedKey + && !string.Equals(draggedKey, channel.Key, StringComparison.Ordinal)) + { + isDragTarget = true; + if (!string.Equals(_dragHoverKey, channel.Key, StringComparison.Ordinal)) + { + _dragHoverKey = channel.Key; + _zoneChatService.MoveChannel(draggedKey, channel.Key); + } + } + + ImGui.EndDragDropTarget(); + } + + var isHoveredDuringDrag = dragActive + && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem | ImGuiHoveredFlags.AllowWhenOverlapped); + + if (!isDragTarget && isHoveredDuringDrag + && !string.Equals(_dragChannelKey, channel.Key, StringComparison.Ordinal)) + { + isDragTarget = true; + if (!string.Equals(_dragHoverKey, channel.Key, StringComparison.Ordinal)) + { + _dragHoverKey = channel.Key; + _zoneChatService.MoveChannel(_dragChannelKey!, channel.Key); + } + } + + var drawList = ImGui.GetWindowDrawList(); + var itemMin = ImGui.GetItemRectMin(); + var itemMax = ImGui.GetItemRectMax(); + + if (isHoveredDuringDrag) + { + var highlight = UIColors.Get("LightlessPurple").WithAlpha(0.35f); + var highlightU32 = ImGui.ColorConvertFloat4ToU32(highlight); + drawList.AddRectFilled(itemMin, itemMax, highlightU32, style.FrameRounding); + drawList.AddRect(itemMin, itemMax, highlightU32, style.FrameRounding, ImDrawFlags.None, Math.Max(1f, ImGuiHelpers.GlobalScale)); + } + + if (isDragTarget) + { + hoveredTargetThisFrame = true; + } + + if (isZoneChannel) + { + var borderColor = UIColors.Get("LightlessOrange"); + var borderColorU32 = ImGui.ColorConvertFloat4ToU32(borderColor); + var borderThickness = Math.Max(1f, ImGuiHelpers.GlobalScale); + drawList.AddRect(itemMin, itemMax, borderColorU32, style.FrameRounding, ImDrawFlags.None, borderThickness); + } + + if (showBadge && badgeMetrics is { } metrics) + { + var buttonSizeY = itemMax.Y - itemMin.Y; + var badgeMin = new Vector2( + itemMin.X + baseFramePadding.X, + itemMin.Y + (buttonSizeY - metrics.Height) * 0.5f); + var badgeMax = badgeMin + new Vector2(metrics.Width, metrics.Height); + var badgeColor = UIColors.Get("DimRed"); + var badgeColorU32 = ImGui.ColorConvertFloat4ToU32(badgeColor); + drawList.AddRectFilled(badgeMin, badgeMax, badgeColorU32, metrics.Height * 0.5f); + var textPos = new Vector2( + badgeMin.X + (metrics.Width - metrics.TextSize.X) * 0.5f, + badgeMin.Y + (metrics.Height - metrics.TextSize.Y) * 0.5f); + drawList.AddText(textPos, ImGui.ColorConvertFloat4ToU32(ImGuiColors.DalamudWhite), metrics.Text); + } + + first = false; + } + + if (dragActive && !hoveredTargetThisFrame) + { + _dragHoverKey = null; + } + + if (_pendingChannelScroll.HasValue) + { + ImGui.SetScrollX(_pendingChannelScroll.Value); + _pendingChannelScroll = null; + } + + currentScroll = ImGui.GetScrollX(); + maxScroll = ImGui.GetScrollMaxX(); + } + } + + if (alignPushed) + { + ImGui.PopStyleVar(); + } + + ImGui.SameLine(0f, style.ItemSpacing.X); + + using (ImRaii.Disabled(!hasChannels || prevScroll >= prevMax - 0.5f)) + { + var arrowNormal = UIColors.Get("ButtonDefault"); + var arrowHovered = UIColors.Get("LightlessPurple").WithAlpha(0.85f); + var arrowActive = UIColors.Get("LightlessPurpleDefault").WithAlpha(0.75f); + ImGui.PushStyleColor(ImGuiCol.Button, arrowNormal); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, arrowHovered); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, arrowActive); + var clickedRight = ImGui.ArrowButton("##chat_right", ImGuiDir.Right); + ImGui.PopStyleColor(3); + if (clickedRight) + { + _pendingChannelScroll = Math.Min(prevScroll + scrollStep, prevMax); + } + } + + ImGui.EndGroup(); + ImGui.PopID(); + + _channelScroll = currentScroll; + _channelScrollMax = maxScroll; + + if (_dragChannelKey is not null && !ImGui.IsMouseDown(ImGuiMouseButton.Left)) + { + _dragChannelKey = null; + _dragHoverKey = null; + } + + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - style.ItemSpacing.Y * 0.3f); + } + + private string GetChannelTabLabel(ChatChannelSnapshot channel) + { + if (channel.Type != ChatChannelType.Group) + { + return channel.DisplayName; + } + + if (!_chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var preferNote) || !preferNote) + { + return channel.DisplayName; + } + + var note = GetChannelNote(channel); + if (string.IsNullOrWhiteSpace(note)) + { + return channel.DisplayName; + } + + return TruncateChannelNoteForTab(note); + } + + private static string TruncateChannelNoteForTab(string note) + { + if (note.Length <= MaxChannelNoteTabLength) + { + return note; + } + + var ellipsis = "..."; + var maxPrefix = Math.Max(0, MaxChannelNoteTabLength - ellipsis.Length); + return note[..maxPrefix] + ellipsis; + } + + private bool ShouldShowChannelTabContextMenu(ChatChannelSnapshot channel) + { + if (channel.Type != ChatChannelType.Group) + { + return false; + } + + if (_chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var preferNote) && preferNote) + { + return true; + } + + var note = GetChannelNote(channel); + return !string.IsNullOrWhiteSpace(note); + } + + private void DrawChannelTabContextMenu(ChatChannelSnapshot channel) + { + var preferNote = _chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var value) && value; + var note = GetChannelNote(channel); + var hasNote = !string.IsNullOrWhiteSpace(note); + if (preferNote || hasNote) + { + var label = preferNote ? "Prefer Name Instead" : "Prefer Note Instead"; + if (ImGui.MenuItem(label)) + { + SetPreferNoteForChannel(channel.Key, !preferNote); + } + } + + if (preferNote) + { + ImGui.Separator(); + ImGui.TextDisabled("Name:"); + ImGui.TextWrapped(channel.DisplayName); + } + + if (hasNote) + { + ImGui.Separator(); + ImGui.TextDisabled("Note:"); + ImGui.TextWrapped(note); + } + } + + private string? GetChannelNote(ChatChannelSnapshot channel) + { + if (channel.Type != ChatChannelType.Group) + { + return null; + } + + var gid = channel.Descriptor.CustomKey; + if (string.IsNullOrWhiteSpace(gid)) + { + return null; + } + + return _serverConfigurationManager.GetNoteForGid(gid); + } + + private void SetPreferNoteForChannel(string channelKey, bool preferNote) + { + if (preferNote) + { + _chatConfigService.Current.PreferNotesForChannels[channelKey] = true; + } + else + { + _chatConfigService.Current.PreferNotesForChannels.Remove(channelKey); + } + + _chatConfigService.Save(); + } + + private void DrawSystemEntry(ChatMessageEntry entry) + { + var system = entry.SystemMessage; + if (system is null) + return; + + switch (system.Type) + { + case ChatSystemEntryType.ZoneSeparator: + DrawZoneSeparatorEntry(system, entry.ReceivedAtUtc); + break; + } + } + + private void DrawZoneSeparatorEntry(ChatSystemEntry systemEntry, DateTime timestampUtc) + { + ImGui.Spacing(); + + var zoneName = string.IsNullOrWhiteSpace(systemEntry.ZoneName) ? "Zone" : systemEntry.ZoneName; + var localTime = timestampUtc.ToLocalTime(); + var label = $"{localTime.ToString("HH:mm", CultureInfo.CurrentCulture)} - {zoneName}"; + var availableWidth = ImGui.GetContentRegionAvail().X; + var textSize = ImGui.CalcTextSize(label); + var cursor = ImGui.GetCursorPos(); + var textPosX = cursor.X + MathF.Max(0f, (availableWidth - textSize.X) * 0.5f); + + ImGui.SetCursorPos(new Vector2(textPosX, cursor.Y)); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey2); + ImGui.TextUnformatted(label); + ImGui.PopStyleColor(); + + var nextY = ImGui.GetCursorPosY() + ImGui.GetStyle().ItemSpacing.Y * 0.35f; + ImGui.SetCursorPos(new Vector2(cursor.X, nextY)); + ImGui.Separator(); + ImGui.Spacing(); + } + + private void DrawContextMenuAction(ChatMessageContextAction action, int index) + { + ImGui.PushID(index); + using var disabled = ImRaii.Disabled(!action.IsEnabled); + + var availableWidth = Math.Max(1f, ImGui.GetContentRegionAvail().X); + var clicked = ImGui.Selectable("##chat_ctx_action", false, ImGuiSelectableFlags.None, new Vector2(availableWidth, 0f)); + + var drawList = ImGui.GetWindowDrawList(); + var itemMin = ImGui.GetItemRectMin(); + var itemMax = ImGui.GetItemRectMax(); + var itemHeight = itemMax.Y - itemMin.Y; + var style = ImGui.GetStyle(); + var textColor = ImGui.GetColorU32(action.IsEnabled ? ImGuiCol.Text : ImGuiCol.TextDisabled); + + var textSize = ImGui.CalcTextSize(action.Label); + var textPos = new Vector2(itemMin.X + style.FramePadding.X, itemMin.Y + (itemHeight - textSize.Y) * 0.5f); + + if (action.Icon.HasValue) + { + var iconSize = _uiSharedService.GetIconSize(action.Icon.Value); + var iconPos = new Vector2( + itemMin.X + style.FramePadding.X, + itemMin.Y + (itemHeight - iconSize.Y) * 0.5f); + + using (_uiSharedService.IconFont.Push()) + { + drawList.AddText(iconPos, textColor, action.Icon.Value.ToIconString()); + } + + textPos.X = iconPos.X + iconSize.X + style.ItemSpacing.X; + } + + drawList.AddText(textPos, textColor, action.Label); + + if (clicked && action.IsEnabled) + { + ImGui.CloseCurrentPopup(); + action.Execute(); + } + + ImGui.PopID(); + } + + private readonly record struct ChatMessageContextAction(FontAwesomeIcon? Icon, string Label, bool IsEnabled, Action Execute); +} diff --git a/LightlessSync/Utils/Crypto.cs b/LightlessSync/Utils/Crypto.cs index c31f82f..09ed636 100644 --- a/LightlessSync/Utils/Crypto.cs +++ b/LightlessSync/Utils/Crypto.cs @@ -1,4 +1,8 @@ -using System.Security.Cryptography; +using Blake3; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Security.Cryptography; using System.Text; namespace LightlessSync.Utils; @@ -9,18 +13,92 @@ public static class Crypto private const int _bufferSize = 65536; #pragma warning disable SYSLIB0021 // Type or member is obsolete - private static readonly Dictionary<(string, ushort), string> _hashListPlayersSHA256 = []; - private static readonly Dictionary _hashListSHA256 = new(StringComparer.Ordinal); + private static readonly ConcurrentDictionary<(string, ushort), string> _hashListPlayersSHA256 = new(); + private static readonly ConcurrentDictionary _hashListSHA256 = new(StringComparer.Ordinal); private static readonly SHA256CryptoServiceProvider _sha256CryptoProvider = new(); - public static string GetFileHash(this string filePath) + // BLAKE3 hash caches + private static readonly Dictionary<(string, ushort), string> _hashListPlayersBlake3 = []; + private static readonly Dictionary _hashListBlake3 = new(StringComparer.Ordinal); + + /// + /// Supports Blake3 or SHA1 for file transfers, no SHA256 supported on it + /// + public enum HashAlgo { - using SHA1 sha1 = SHA1.Create(); - using FileStream stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); - return BitConverter.ToString(sha1.ComputeHash(stream)).Replace("-", "", StringComparison.Ordinal); + Blake3, + Sha1 } - public static async Task GetFileHashAsync(string filePath, CancellationToken cancellationToken = default) + /// + /// Detects which algo is being used for the file + /// + /// Hashed string + /// HashAlgo + public static HashAlgo DetectAlgo(string hashHex) + { + if (hashHex.Length == 40) + return HashAlgo.Sha1; + + return HashAlgo.Blake3; + } + + #region File Hashing + + /// + /// Compute file hash with given algorithm, supports BLAKE3 and Sha1 for file hashing + /// + /// Filepath for the hashing + /// BLAKE3 or Sha1 + /// Hashed file hash + /// Not a valid HashAlgo or Filepath + public static string ComputeFileHash(string filePath, HashAlgo algo) + { + return algo switch + { + HashAlgo.Blake3 => ComputeFileHashBlake3(filePath), + HashAlgo.Sha1 => ComputeFileHashSha1(filePath), + _ => throw new ArgumentOutOfRangeException(nameof(algo), algo, null) + }; + } + + /// + /// Compute file hash asynchronously with given algorithm, supports BLAKE3 and SHA1 for file hashing + /// + /// Filepath for the hashing + /// BLAKE3 or Sha1 + /// Hashed file hash + /// Not a valid HashAlgo or Filepath + public static async Task ComputeFileHashAsync(string filePath, HashAlgo algo, CancellationToken cancellationToken = default) + { + return algo switch + { + HashAlgo.Blake3 => await ComputeFileHashBlake3Async(filePath, cancellationToken).ConfigureAwait(false), + HashAlgo.Sha1 => await ComputeFileHashSha1Async(filePath, cancellationToken).ConfigureAwait(false), + _ => throw new ArgumentOutOfRangeException(nameof(algo), algo, message: null) + }; + } + + /// + /// Computes an file hash with SHA1 + /// + /// Filepath that has to be computed + /// Hashed file in hex string + private static string ComputeFileHashSha1(string filePath) + { + using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + using var sha1 = SHA1.Create(); + var hash = sha1.ComputeHash(stream); + return Convert.ToHexString(hash); + } + + /// + /// Computes an file hash with SHA1 asynchronously + /// + /// Filepath that has to be computed + /// Cancellation token + /// Hashed file in hex string hashed in SHA1 + private static async Task ComputeFileHashSha1Async(string filePath, CancellationToken cancellationToken) { var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, bufferSize: _bufferSize, options: FileOptions.Asynchronous); await using (stream.ConfigureAwait(false)) @@ -29,38 +107,135 @@ public static class Crypto var buffer = new byte[8192]; int bytesRead; - while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0) + while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0) { sha1.TransformBlock(buffer, 0, bytesRead, outputBuffer: null, 0); } sha1.TransformFinalBlock([], 0, 0); - return Convert.ToHexString(sha1.Hash!); } } + /// + /// Computes an file hash with Blake3 + /// + /// Filepath that has to be computed + /// Hashed file in hex string hashed in Blake3 + private static string ComputeFileHashBlake3(string filePath) + { + using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + using var hasher = Hasher.New(); + + var buffer = new byte[_bufferSize]; + int bytesRead; + while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) + { + hasher.Update(buffer.AsSpan(0, bytesRead)); + } + + var hash = hasher.Finalize(); + return hash.ToString(); + } + + + /// + /// Computes an file hash with Blake3 asynchronously + /// + /// Filepath that has to be computed + /// Hashed file in hex string hashed in Blake3 + private static async Task ComputeFileHashBlake3Async(string filePath, CancellationToken cancellationToken) + { + var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, bufferSize: _bufferSize, options: FileOptions.Asynchronous); + await using (stream.ConfigureAwait(false)) + { + using var hasher = Hasher.New(); + + var buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0) + { + hasher.Update(buffer.AsSpan(0, bytesRead)); + } + + var hash = hasher.Finalize(); + return hash.ToString(); + } + } + #endregion + + + #region String hashing + + public static string GetBlake3Hash(this (string, ushort) playerToHash) + { + if (_hashListPlayersBlake3.TryGetValue(playerToHash, out var hash)) + return hash; + + var toHash = playerToHash.Item1 + playerToHash.Item2.ToString(); + + hash = ComputeBlake3Hex(toHash); + _hashListPlayersBlake3[playerToHash] = hash; + return hash; + } + + /// + /// Computes or gets an Blake3 hash(ed) string. + /// + /// String that needs to be hashsed + /// Hashed string + public static string GetBlake3Hash(this string stringToHash) + { + return GetOrComputeBlake3(stringToHash); + } + + private static string GetOrComputeBlake3(string stringToCompute) + { + if (_hashListBlake3.TryGetValue(stringToCompute, out var hash)) + return hash; + + hash = ComputeBlake3Hex(stringToCompute); + _hashListBlake3[stringToCompute] = hash; + return hash; + } + + private static string ComputeBlake3Hex(string input) + { + var bytes = Encoding.UTF8.GetBytes(input); + + var hash = Hasher.Hash(bytes); + + return Convert.ToHexString(hash.AsSpan()); + } + public static string GetHash256(this (string, ushort) playerToHash) { if (_hashListPlayersSHA256.TryGetValue(playerToHash, out var hash)) return hash; return _hashListPlayersSHA256[playerToHash] = - BitConverter.ToString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(playerToHash.Item1 + playerToHash.Item2.ToString()))).Replace("-", "", StringComparison.Ordinal); + Convert.ToHexString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(playerToHash.Item1 + playerToHash.Item2.ToString()))); } + /// + /// Computes or gets an SHA256 hash(ed) string. + /// + /// String that needs to be hashsed + /// Hashed string public static string GetHash256(this string stringToHash) { - return GetOrComputeHashSHA256(stringToHash); + return _hashListSHA256.GetOrAdd(stringToHash, ComputeHashSHA256); } - private static string GetOrComputeHashSHA256(string stringToCompute) + private static string ComputeHashSHA256(string stringToCompute) { if (_hashListSHA256.TryGetValue(stringToCompute, out var hash)) return hash; return _hashListSHA256[stringToCompute] = - BitConverter.ToString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToCompute))).Replace("-", "", StringComparison.Ordinal); - } + Convert.ToHexString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToCompute))); + } + + #endregion #pragma warning restore SYSLIB0021 // Type or member is obsolete } \ No newline at end of file diff --git a/LightlessSync/Utils/FileSystemHelper.cs b/LightlessSync/Utils/FileSystemHelper.cs index d63b3b9..b27fb1c 100644 --- a/LightlessSync/Utils/FileSystemHelper.cs +++ b/LightlessSync/Utils/FileSystemHelper.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using System.Collections.Concurrent; -using System.Diagnostics; using System.Runtime.InteropServices; namespace LightlessSync.Utils @@ -32,7 +31,7 @@ namespace LightlessSync.Utils { string rootPath; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && (!IsProbablyWine() || !isWine)) + if (OperatingSystem.IsWindows() && (!IsProbablyWine() || !isWine)) { var info = new FileInfo(filePath); var dir = info.Directory ?? new DirectoryInfo(filePath); @@ -50,7 +49,7 @@ namespace LightlessSync.Utils FilesystemType detected; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && (!IsProbablyWine() || !isWine)) + if (OperatingSystem.IsWindows() && (!IsProbablyWine() || !isWine)) { var root = new DriveInfo(rootPath); var format = root.DriveFormat?.ToUpperInvariant() ?? string.Empty; @@ -157,7 +156,7 @@ namespace LightlessSync.Utils return mountOptions; } - catch (Exception ex) + catch (Exception) { return string.Empty; } @@ -214,7 +213,7 @@ namespace LightlessSync.Utils if (_blockSizeCache.TryGetValue(root, out int cached)) return cached; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !isWine) + if (OperatingSystem.IsWindows() && !isWine) { int result = GetDiskFreeSpaceW(root, out uint sectorsPerCluster, @@ -234,40 +233,6 @@ namespace LightlessSync.Utils return clusterSize; } - string realPath = fi.FullName; - if (isWine && realPath.StartsWith("Z:\\", StringComparison.OrdinalIgnoreCase)) - { - realPath = "/" + realPath.Substring(3).Replace('\\', '/'); - } - - var psi = new ProcessStartInfo - { - FileName = "/bin/bash", - Arguments = $"-c \"stat -f -c %s '{realPath.Replace("'", "'\\''")}'\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - WorkingDirectory = "/" - }; - - using var proc = Process.Start(psi); - - string stdout = proc?.StandardOutput.ReadToEnd().Trim() ?? ""; - string _stderr = proc?.StandardError.ReadToEnd() ?? ""; - - try { proc?.WaitForExit(); } - catch (Exception ex) { logger?.LogTrace(ex, "stat WaitForExit failed under Wine; ignoring"); } - - if (!(!int.TryParse(stdout, out int block) || block <= 0)) - { - _blockSizeCache[root] = block; - logger?.LogTrace("Filesystem block size via stat for {root}: {block}", root, block); - return block; - } - - logger?.LogTrace("stat did not return valid block size for {file}, output: {out}", fi.FullName, stdout); - _blockSizeCache[root] = _defaultBlockSize; return _defaultBlockSize; } catch (Exception ex) diff --git a/LightlessSync/Utils/SeStringUtils.cs b/LightlessSync/Utils/SeStringUtils.cs index 7507515..2188d91 100644 --- a/LightlessSync/Utils/SeStringUtils.cs +++ b/LightlessSync/Utils/SeStringUtils.cs @@ -1,13 +1,15 @@ using Dalamud.Bindings.ImGui; using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.ImGuiSeStringRenderer; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; -using Lumina.Text; -using System; +using Lumina.Text.Parse; +using Lumina.Text.ReadOnly; +using System.Globalization; using System.Numerics; -using System.Threading; +using System.Reflection; +using System.Text; using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString; using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder; using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder; @@ -19,6 +21,438 @@ public static class SeStringUtils private static int _seStringHitboxCounter; private static int _iconHitboxCounter; + public static bool TryRenderSeStringMarkupAtCursor(string payload) + { + if (string.IsNullOrWhiteSpace(payload)) + return false; + + var wrapWidth = ImGui.GetContentRegionAvail().X; + if (wrapWidth <= 0f || float.IsNaN(wrapWidth) || float.IsInfinity(wrapWidth)) + wrapWidth = float.MaxValue; + + var normalizedPayload = payload.ReplaceLineEndings("
"); + try + { + _ = ReadOnlySeString.FromMacroString(normalizedPayload, new MacroStringParseOptions + { + ExceptionMode = MacroStringParseExceptionMode.Throw + }); + } + catch (Exception) + { + return false; + } + + try + { + var drawParams = new SeStringDrawParams + { + WrapWidth = wrapWidth, + Font = ImGui.GetFont(), + Color = ImGui.GetColorU32(ImGuiCol.Text), + }; + + var renderId = ImGui.GetID($"SeStringMarkup##{normalizedPayload.GetHashCode(StringComparison.Ordinal)}"); + var drawResult = ImGuiHelpers.CompileSeStringWrapped(normalizedPayload, drawParams, renderId); + var height = drawResult.Size.Y; + if (height <= 0f) + height = ImGui.GetTextLineHeight(); + + ImGui.Dummy(new Vector2(0f, height)); + + if (drawResult.InteractedPayloadEnvelope.Length > 0 && + TryExtractLink(drawResult.InteractedPayloadEnvelope, drawResult.InteractedPayload, out var linkUrl, out var tooltipText)) + { + var hoverText = !string.IsNullOrEmpty(linkUrl) ? linkUrl : tooltipText; + + if (!string.IsNullOrEmpty(hoverText)) + { + ImGui.BeginTooltip(); + ImGui.TextUnformatted(hoverText); + ImGui.EndTooltip(); + } + + if (!string.IsNullOrEmpty(linkUrl)) + ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); + + if (drawResult.Clicked && !string.IsNullOrEmpty(linkUrl)) + Dalamud.Utility.Util.OpenLink(linkUrl); + } + + return true; + } + catch (Exception ex) + { + ImGui.TextDisabled($"[SeString error] {ex.Message}"); + return false; + } + } + + public enum SeStringSegmentType + { + Text, + Icon + } + + public readonly record struct SeStringSegment( + SeStringSegmentType Type, + string? Text, + Vector4? Color, + uint IconId, + IDalamudTextureWrap? Texture, + Vector2 Size); + + public static bool TryResolveSegments( + string payload, + float scale, + Func iconResolver, + List resolvedSegments, + out Vector2 totalSize) + { + totalSize = Vector2.Zero; + if (string.IsNullOrWhiteSpace(payload)) + return false; + + var parsedSegments = new List(Math.Max(1, payload.Length / 4)); + if (!ParseSegments(payload, parsedSegments)) + return false; + + float width = 0f; + float height = 0f; + + foreach (var segment in parsedSegments) + { + switch (segment.Type) + { + case ParsedSegmentType.Text: + { + var text = segment.Text ?? string.Empty; + if (text.Length == 0) + break; + + var textSize = ImGui.CalcTextSize(text); + resolvedSegments.Add(new SeStringSegment(SeStringSegmentType.Text, text, segment.Color, 0, null, textSize)); + width += textSize.X; + height = MathF.Max(height, textSize.Y); + break; + } + case ParsedSegmentType.Icon: + { + var wrap = iconResolver(segment.IconId); + Vector2 iconSize; + string? fallback = null; + if (wrap != null) + { + iconSize = CalculateIconSize(wrap, scale); + } + else + { + fallback = $"[{segment.IconId}]"; + iconSize = ImGui.CalcTextSize(fallback); + } + + resolvedSegments.Add(new SeStringSegment(SeStringSegmentType.Icon, fallback, segment.Color, segment.IconId, wrap, iconSize)); + width += iconSize.X; + height = MathF.Max(height, iconSize.Y); + break; + } + } + } + + totalSize = new Vector2(width, height); + parsedSegments.Clear(); + return resolvedSegments.Count > 0; + } + + private enum ParsedSegmentType + { + Text, + Icon + } + + private readonly record struct ParsedSegment( + ParsedSegmentType Type, + string? Text, + uint IconId, + Vector4? Color); + + private static bool ParseSegments(string payload, List segments) + { + var builder = new StringBuilder(payload.Length); + Vector4? activeColor = null; + var index = 0; + while (index < payload.Length) + { + if (payload[index] == '<') + { + var end = payload.IndexOf('>', index); + if (end == -1) + break; + + var tagContent = payload.Substring(index + 1, end - index - 1); + if (TryHandleIconTag(tagContent, segments, builder, activeColor)) + { + index = end + 1; + continue; + } + + if (TryHandleColorTag(tagContent, segments, builder, ref activeColor)) + { + index = end + 1; + continue; + } + + builder.Append('<'); + builder.Append(tagContent); + builder.Append('>'); + index = end + 1; + } + else + { + builder.Append(payload[index]); + index++; + } + } + + if (index < payload.Length) + builder.Append(payload, index, payload.Length - index); + + FlushTextBuilder(builder, activeColor, segments); + return segments.Count > 0; + } + + private static bool TryHandleIconTag(string tagContent, List segments, StringBuilder textBuilder, Vector4? activeColor) + { + if (!tagContent.StartsWith("icon(", StringComparison.OrdinalIgnoreCase) || !tagContent.EndsWith(')')) + return false; + + var inner = tagContent.AsSpan(5, tagContent.Length - 6).Trim(); + if (!uint.TryParse(inner, NumberStyles.Integer, CultureInfo.InvariantCulture, out var iconId)) + return false; + + FlushTextBuilder(textBuilder, activeColor, segments); + segments.Add(new ParsedSegment(ParsedSegmentType.Icon, null, iconId, null)); + return true; + } + + private static bool TryHandleColorTag(string tagContent, List segments, StringBuilder textBuilder, ref Vector4? activeColor) + { + if (tagContent.StartsWith("color", StringComparison.OrdinalIgnoreCase)) + { + var equalsIndex = tagContent.IndexOf('='); + if (equalsIndex == -1) + return false; + + var value = tagContent.Substring(equalsIndex + 1).Trim().Trim('"'); + if (!TryParseColor(value, out var color)) + return false; + + FlushTextBuilder(textBuilder, activeColor, segments); + activeColor = color; + return true; + } + + if (tagContent.Equals("/color", StringComparison.OrdinalIgnoreCase)) + { + FlushTextBuilder(textBuilder, activeColor, segments); + activeColor = null; + return true; + } + + return false; + } + + private static void FlushTextBuilder(StringBuilder builder, Vector4? color, List segments) + { + if (builder.Length == 0) + return; + + segments.Add(new ParsedSegment(ParsedSegmentType.Text, builder.ToString(), 0, color)); + builder.Clear(); + } + + private static bool TryExtractLink(ReadOnlySpan envelope, Payload? payload, out string url, out string? tooltipText) + { + url = string.Empty; + tooltipText = null; + + if (envelope.Length == 0 && payload is null) + return false; + + tooltipText = envelope.Length > 0 ? DalamudSeString.Parse(envelope.ToArray()).TextValue : null; + + if (payload is not null && TryReadUrlFromPayload(payload, out url)) + return true; + + if (!string.IsNullOrWhiteSpace(tooltipText)) + { + var candidate = FindFirstUrl(tooltipText); + if (!string.IsNullOrEmpty(candidate)) + { + url = candidate; + return true; + } + } + + url = string.Empty; + return false; + } + + private static bool TryReadUrlFromPayload(object payload, out string url) + { + url = string.Empty; + var type = payload.GetType(); + + static string? ReadStringProperty(Type type, object instance, string propertyName) + => type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + ?.GetValue(instance) as string; + + string? candidate = ReadStringProperty(type, payload, "Uri") + ?? ReadStringProperty(type, payload, "Url") + ?? ReadStringProperty(type, payload, "Target") + ?? ReadStringProperty(type, payload, "Destination"); + + if (IsHttpUrl(candidate)) + { + url = candidate!; + return true; + } + + var dataProperty = type.GetProperty("Data", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (dataProperty?.GetValue(payload) is IEnumerable sequence) + { + foreach (var entry in sequence) + { + if (IsHttpUrl(entry)) + { + url = entry; + return true; + } + } + } + + var extraStringProp = type.GetProperty("ExtraString", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (IsHttpUrl(extraStringProp?.GetValue(payload) as string)) + { + url = (extraStringProp!.GetValue(payload) as string)!; + return true; + } + + var textProp = type.GetProperty("Text", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (IsHttpUrl(textProp?.GetValue(payload) as string)) + { + url = (textProp!.GetValue(payload) as string)!; + return true; + } + + return false; + } + + private static string? FindFirstUrl(string? text) + { + if (string.IsNullOrWhiteSpace(text)) + return null; + + var index = text.IndexOf("http", StringComparison.OrdinalIgnoreCase); + while (index >= 0) + { + var end = index; + while (end < text.Length && !char.IsWhiteSpace(text[end]) && !"\"')]>".Contains(text[end])) + end++; + + var candidate = text.Substring(index, end - index).TrimEnd('.', ',', ';'); + if (IsHttpUrl(candidate)) + return candidate; + + index = text.IndexOf("http", end, StringComparison.OrdinalIgnoreCase); + } + + return null; + } + + private static bool IsHttpUrl(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return false; + + return Uri.TryCreate(value, UriKind.Absolute, out var uri) + && (string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.Ordinal) || string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.Ordinal)); + } + + public static string StripMarkup(string value) + { + if (string.IsNullOrEmpty(value)) + return string.Empty; + + var builder = new StringBuilder(value.Length); + int depth = 0; + foreach (var c in value) + { + if (c == '<') + { + depth++; + continue; + } + + if (c == '>' && depth > 0) + { + depth--; + continue; + } + + if (depth == 0) + builder.Append(c); + } + + return builder.ToString().Trim(); + } + + private static bool TryParseColor(string value, out Vector4 color) + { + color = default; + if (string.IsNullOrEmpty(value) || value[0] != '#') + return false; + + var hex = value.AsSpan(1); + if (!uint.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed)) + return false; + + byte a, r, g, b; + if (hex.Length == 6) + { + a = 0xFF; + r = (byte)(parsed >> 16); + g = (byte)(parsed >> 8); + b = (byte)parsed; + } + else if (hex.Length == 8) + { + a = (byte)(parsed >> 24); + r = (byte)(parsed >> 16); + g = (byte)(parsed >> 8); + b = (byte)parsed; + } + else + { + return false; + } + + const float inv = 1.0f / 255f; + color = new Vector4(r * inv, g * inv, b * inv, a * inv); + return true; + } + + private static Vector2 CalculateIconSize(IDalamudTextureWrap wrap, float scale) + { + const float IconHeightScale = 1.25f; + + var textHeight = ImGui.GetTextLineHeight(); + var baselineHeight = MathF.Max(1f, textHeight - 2f * scale); + var targetHeight = MathF.Max(baselineHeight, baselineHeight * IconHeightScale); + var aspect = wrap.Width > 0 ? wrap.Width / (float)wrap.Height : 1f; + return new Vector2(targetHeight * aspect, targetHeight); + } + public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor) { var b = new DalamudSeStringBuilder(); @@ -57,10 +491,9 @@ public static class SeStringUtils continue; var hasColor = fragment.Color.HasValue; - Vector4 color = default; if (hasColor) { - color = fragment.Color!.Value; + Vector4 color = fragment.Color!.Value; builder.PushColorRgba(color); } @@ -106,12 +539,15 @@ public static class SeStringUtils { drawList ??= ImGui.GetWindowDrawList(); + var usedFont = font ?? ImGui.GetFont(); var drawParams = new SeStringDrawParams { - Font = font ?? ImGui.GetFont(), + Font = usedFont, + FontSize = usedFont.FontSize, Color = ImGui.GetColorU32(ImGuiCol.Text), WrapWidth = wrapWidth, - TargetDrawList = drawList + TargetDrawList = drawList, + ScreenOffset = ImGui.GetCursorScreenPos() }; ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); @@ -123,22 +559,38 @@ public static class SeStringUtils ImGui.Dummy(new Vector2(0f, textSize.Y)); } + public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, string? id = null) { var drawList = ImGui.GetWindowDrawList(); + var usedFont = font ?? UiBuilder.MonoFont; + var textSize = ImGui.CalcTextSize(seString.TextValue); + if (textSize.Y <= 0f) + { + textSize.Y = usedFont.FontSize; + } + + var style = ImGui.GetStyle(); + var fontHeight = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize(); + var frameHeight = fontHeight + style.FramePadding.Y * 2f; + var hitboxHeight = MathF.Max(frameHeight, textSize.Y); + var verticalOffset = MathF.Max((hitboxHeight - textSize.Y) * 0.5f, 0f); + + var drawPos = new Vector2(position.X, position.Y + verticalOffset); var drawParams = new SeStringDrawParams { - Font = font ?? UiBuilder.MonoFont, + FontSize = usedFont.FontSize, + ScreenOffset = drawPos, + Font = usedFont, Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = drawList }; - ImGui.SetCursorScreenPos(position); - ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); + ImGui.SetCursorScreenPos(drawPos); - var textSize = ImGui.CalcTextSize(seString.TextValue); + ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); ImGui.SetCursorScreenPos(position); if (id is not null) @@ -152,30 +604,112 @@ public static class SeStringUtils try { - ImGui.InvisibleButton("##hitbox", textSize); + ImGui.InvisibleButton("##hitbox", new Vector2(textSize.X, hitboxHeight)); } finally { ImGui.PopID(); } - return textSize; + return new Vector2(textSize.X, hitboxHeight); + } + + public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, float? targetFontSize, ImFontPtr? font = null, string? id = null) + { + var drawList = ImGui.GetWindowDrawList(); + var usedFont = font ?? ImGui.GetFont(); + + ImGui.PushFont(usedFont); + Vector2 rawSize; + float usedEffectiveSize; + try + { + usedEffectiveSize = ImGui.GetFontSize(); + rawSize = ImGui.CalcTextSize(seString.TextValue); + } + finally + { + ImGui.PopFont(); + } + + var desiredSize = targetFontSize ?? usedEffectiveSize; + var scale = usedEffectiveSize > 0 ? (desiredSize / usedEffectiveSize) : 1f; + + var textSize = rawSize * scale; + + var style = ImGui.GetStyle(); + var frameHeight = desiredSize + style.FramePadding.Y * 2f; + var hitboxHeight = MathF.Max(frameHeight, textSize.Y); + var verticalOffset = MathF.Max((hitboxHeight - textSize.Y) * 0.5f, 0f); + + var drawPos = new Vector2(position.X, position.Y + verticalOffset); + + var drawParams = new SeStringDrawParams + { + TargetDrawList = drawList, + ScreenOffset = drawPos, + Font = usedFont, + FontSize = desiredSize, + Color = 0xFFFFFFFF, + WrapWidth = float.MaxValue, + }; + + ImGui.SetCursorScreenPos(drawPos); + ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); + + ImGui.SetCursorScreenPos(position); + ImGui.PushID(id ?? Interlocked.Increment(ref _seStringHitboxCounter).ToString()); + + try + { + ImGui.InvisibleButton("##hitbox", new Vector2(textSize.X, hitboxHeight)); + } + finally + { + ImGui.PopID(); + } + + return new Vector2(textSize.X, hitboxHeight); } public static Vector2 RenderIconWithHitbox(int iconId, Vector2 position, ImFontPtr? font = null, string? id = null) { var drawList = ImGui.GetWindowDrawList(); + var usedFont = font ?? UiBuilder.MonoFont; + var iconMacro = $""; - var drawParams = new SeStringDrawParams + var measureParams = new SeStringDrawParams { - Font = font ?? UiBuilder.MonoFont, + Font = usedFont, + FontSize = usedFont.FontSize, Color = 0xFFFFFFFF, - WrapWidth = float.MaxValue, - TargetDrawList = drawList + WrapWidth = float.MaxValue }; - var iconMacro = $""; - var drawResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams); + var measureResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, measureParams); + var iconSize = measureResult.Size; + if (iconSize.Y <= 0f) + { + iconSize.Y = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize(); + } + + var style = ImGui.GetStyle(); + var fontHeight = usedFont.FontSize > 0f ? usedFont.FontSize : ImGui.GetFontSize(); + var frameHeight = fontHeight + style.FramePadding.Y * 2f; + var hitboxHeight = MathF.Max(frameHeight, iconSize.Y); + var verticalOffset = MathF.Max((hitboxHeight - iconSize.Y) * 0.5f, 0f); + + var drawPos = new Vector2(position.X, position.Y + verticalOffset); + var drawParams = new SeStringDrawParams + { + Font = usedFont, + FontSize = usedFont.FontSize, + Color = 0xFFFFFFFF, + WrapWidth = float.MaxValue, + TargetDrawList = drawList, + ScreenOffset = drawPos + }; + ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams); ImGui.SetCursorScreenPos(position); if (id is not null) @@ -189,14 +723,14 @@ public static class SeStringUtils try { - ImGui.InvisibleButton("##iconHitbox", drawResult.Size); + ImGui.InvisibleButton("##iconHitbox", new Vector2(iconSize.X, hitboxHeight)); } finally { ImGui.PopID(); } - return drawResult.Size; + return new Vector2(iconSize.X, hitboxHeight); } #region Internal Payloads @@ -233,7 +767,7 @@ public static class SeStringUtils protected abstract byte ChunkType { get; } } - private class ColorPayload : AbstractColorPayload + private sealed class ColorPayload : AbstractColorPayload { protected override byte ChunkType => 0x13; @@ -247,12 +781,12 @@ public static class SeStringUtils public ColorPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { } } - private class ColorEndPayload : AbstractColorEndPayload + private sealed class ColorEndPayload : AbstractColorEndPayload { protected override byte ChunkType => 0x13; } - private class GlowPayload : AbstractColorPayload + private sealed class GlowPayload : AbstractColorPayload { protected override byte ChunkType => 0x14; @@ -266,7 +800,7 @@ public static class SeStringUtils public GlowPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { } } - private class GlowEndPayload : AbstractColorEndPayload + private sealed class GlowEndPayload : AbstractColorEndPayload { protected override byte ChunkType => 0x14; } diff --git a/LightlessSync/Utils/VariousExtensions.cs b/LightlessSync/Utils/VariousExtensions.cs index 9215893..3f47d98 100644 --- a/LightlessSync/Utils/VariousExtensions.cs +++ b/LightlessSync/Utils/VariousExtensions.cs @@ -1,9 +1,10 @@ using Dalamud.Game.ClientState.Objects.Types; using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; -using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Pairs; using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Linq; using System.Text.Json; namespace LightlessSync.Utils; @@ -56,9 +57,20 @@ public static class VariousExtensions } public static Dictionary> CheckUpdatedData(this CharacterData newData, Guid applicationBase, - CharacterData? oldData, ILogger logger, PairHandler cachedPlayer, bool forceApplyCustomization, bool forceApplyMods) + CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods) { oldData ??= new(); + static bool FileReplacementsEquivalent(ICollection left, ICollection right) + { + if (left.Count != right.Count) + { + return false; + } + + var comparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer.Instance; + return !left.Except(right, comparer).Any() && !right.Except(left, comparer).Any(); + } + var charaDataToUpdate = new Dictionary>(); foreach (ObjectKind objectKind in Enum.GetValues()) { @@ -91,7 +103,9 @@ public static class VariousExtensions { if (hasNewAndOldFileReplacements) { - bool listsAreEqual = oldData.FileReplacements[objectKind].SequenceEqual(newData.FileReplacements[objectKind], PlayerData.Data.FileReplacementDataComparer.Instance); + var oldList = oldData.FileReplacements[objectKind]; + var newList = newData.FileReplacements[objectKind]; + var listsAreEqual = FileReplacementsEquivalent(oldList, newList); if (!listsAreEqual || forceApplyMods) { logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles); @@ -114,9 +128,9 @@ public static class VariousExtensions .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); var newTail = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); - var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl"))) + var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); - var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl"))) + var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); logger.LogTrace("[BASE-{appbase}] ExistingFace: {of}, NewFace: {fc}; ExistingHair: {eh}, NewHair: {nh}; ExistingTail: {et}, NewTail: {nt}; ExistingTransient: {etr}, NewTransient: {ntr}", applicationBase, @@ -163,7 +177,8 @@ public static class VariousExtensions if (objectKind != ObjectKind.Player) continue; bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal); - if (manipDataDifferent || forceApplyMods) + var hasManipulationData = !string.IsNullOrEmpty(newData.ManipulationData); + if (manipDataDifferent || (forceApplyMods && hasManipulationData)) { logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip); charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip); diff --git a/LightlessSync/Utils/WindowUtils.cs b/LightlessSync/Utils/WindowUtils.cs new file mode 100644 index 0000000..fb88a84 --- /dev/null +++ b/LightlessSync/Utils/WindowUtils.cs @@ -0,0 +1,139 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; +using LightlessSync.UI; +using LightlessSync.WebAPI.SignalR.Utils; +using System.Numerics; + +namespace LightlessSync.Utils; + +public sealed class WindowBuilder +{ + private readonly Window _window; + private readonly List _titleButtons = new(); + + private WindowBuilder(Window window) + { + _window = window ?? throw new ArgumentNullException(nameof(window)); + } + + public static WindowBuilder For(Window window) => new(window); + + public WindowBuilder AllowPinning(bool allow = true) + { + _window.AllowPinning = allow; + return this; + } + + public WindowBuilder AllowClickthrough(bool allow = true) + { + _window.AllowClickthrough = allow; + return this; + } + + public WindowBuilder SetFixedSize(Vector2 size) => SetSizeConstraints(size, size); + + public WindowBuilder SetSizeConstraints(Vector2 min, Vector2 max) + { + _window.SizeConstraints = new Window.WindowSizeConstraints + { + MinimumSize = min, + MaximumSize = max, + }; + return this; + } + + public WindowBuilder AddFlags(ImGuiWindowFlags flags) + { + _window.Flags |= flags; + return this; + } + + public WindowBuilder AddTitleBarButton(FontAwesomeIcon icon, string tooltip, Action onClick, Vector2? iconOffset = null) + { + _titleButtons.Add(new Window.TitleBarButton + { + Icon = icon, + IconOffset = iconOffset ?? new Vector2(2, 1), + Click = _ => onClick(), + ShowTooltip = () => UiSharedService.AttachToolTip(tooltip), + }); + return this; + } + + public Window Apply() + { + if (_titleButtons.Count > 0) + _window.TitleBarButtons = _titleButtons; + return _window; + } +} + +public static class WindowUtils +{ + public static Vector4 GetUidColor(this ServerState state) + { + return state switch + { + ServerState.Connecting => UIColors.Get("LightlessYellow"), + ServerState.Reconnecting => UIColors.Get("DimRed"), + ServerState.Connected => UIColors.Get("LightlessPurple"), + ServerState.Disconnected => UIColors.Get("LightlessYellow"), + ServerState.Disconnecting => UIColors.Get("LightlessYellow"), + ServerState.Unauthorized => UIColors.Get("DimRed"), + ServerState.VersionMisMatch => UIColors.Get("DimRed"), + ServerState.Offline => UIColors.Get("DimRed"), + ServerState.RateLimited => UIColors.Get("LightlessYellow"), + ServerState.NoSecretKey => UIColors.Get("LightlessYellow"), + ServerState.MultiChara => UIColors.Get("LightlessYellow"), + ServerState.OAuthMisconfigured => UIColors.Get("DimRed"), + ServerState.OAuthLoginTokenStale => UIColors.Get("DimRed"), + ServerState.NoAutoLogon => UIColors.Get("LightlessYellow"), + _ => UIColors.Get("DimRed"), + }; + } + + public static string GetUidText(this ServerState state, string displayName) + { + return state switch + { + ServerState.Reconnecting => "Reconnecting", + ServerState.Connecting => "Connecting", + ServerState.Disconnected => "Disconnected", + ServerState.Disconnecting => "Disconnecting", + ServerState.Unauthorized => "Unauthorized", + ServerState.VersionMisMatch => "Version mismatch", + ServerState.Offline => "Unavailable", + ServerState.RateLimited => "Rate Limited", + ServerState.NoSecretKey => "No Secret Key", + ServerState.MultiChara => "Duplicate Characters", + ServerState.OAuthMisconfigured => "Misconfigured OAuth2", + ServerState.OAuthLoginTokenStale => "Stale OAuth2", + ServerState.NoAutoLogon => "Auto Login disabled", + ServerState.Connected => displayName, + _ => string.Empty, + }; + } + + public static string GetServerError(this ServerState state, string authFailureMessage) + { + return state switch + { + ServerState.Connecting => "Attempting to connect to the server.", + ServerState.Reconnecting => "Connection to server interrupted, attempting to reconnect to the server.", + ServerState.Disconnected => "You are currently disconnected from the Lightless Sync server.", + ServerState.Disconnecting => "Disconnecting from the server", + ServerState.Unauthorized => "Server Response: " + authFailureMessage, + ServerState.Offline => "Your selected Lightless Sync server is currently offline.", + ServerState.VersionMisMatch => + "Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.", + ServerState.RateLimited => "You are rate limited for (re)connecting too often. Disconnect, wait 10 minutes and try again.", + ServerState.NoSecretKey => "You have no secret key set for this current character. Open Settings -> Service Settings and set a secret key for the current character. You can reuse the same secret key for multiple characters.", + ServerState.MultiChara => "Your Character Configuration has multiple characters configured with same name and world. You will not be able to connect until you fix this issue. Remove the duplicates from the configuration in Settings -> Service Settings -> Character Management and reconnect manually after.", + ServerState.OAuthMisconfigured => "OAuth2 is enabled but not fully configured, verify in the Settings -> Service Settings that you have OAuth2 connected and, importantly, a UID assigned to your current character.", + ServerState.OAuthLoginTokenStale => "Your OAuth2 login token is stale and cannot be used to renew. Go to the Settings -> Service Settings and unlink then relink your OAuth2 configuration.", + ServerState.NoAutoLogon => "This character has automatic login into Lightless disabled. Press the connect button to connect to Lightless.", + _ => string.Empty, + }; + } +} diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index b8f81f2..49dd868 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -1,21 +1,16 @@ -using Dalamud.Utility; using K4os.Compression.LZ4.Legacy; using LightlessSync.API.Data; using LightlessSync.API.Dto.Files; using LightlessSync.API.Routes; using LightlessSync.FileCache; using LightlessSync.PlayerData.Handlers; -using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Services.TextureCompression; using LightlessSync.WebAPI.Files.Models; using Microsoft.Extensions.Logging; -using System; using System.Collections.Concurrent; -using System.IO; using System.Net; using System.Net.Http.Json; -using System.Threading; -using System.Threading.Tasks; using LightlessSync.LightlessConfiguration; namespace LightlessSync.WebAPI.Files; @@ -26,25 +21,30 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase private readonly FileCompactor _fileCompactor; private readonly FileCacheManager _fileDbManager; private readonly FileTransferOrchestrator _orchestrator; - private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly LightlessConfigService _configService; + private readonly TextureDownscaleService _textureDownscaleService; + private readonly TextureMetadataHelper _textureMetadataHelper; private readonly ConcurrentDictionary _activeDownloadStreams; - private static readonly TimeSpan DownloadStallTimeout = TimeSpan.FromSeconds(30); private volatile bool _disableDirectDownloads; private int _consecutiveDirectDownloadFailures; private bool _lastConfigDirectDownloadsState; - public FileDownloadManager(ILogger logger, LightlessMediator mediator, + public FileDownloadManager( + ILogger logger, + LightlessMediator mediator, FileTransferOrchestrator orchestrator, - FileCacheManager fileCacheManager, FileCompactor fileCompactor, - PairProcessingLimiter pairProcessingLimiter, LightlessConfigService configService) : base(logger, mediator) + FileCacheManager fileCacheManager, + FileCompactor fileCompactor, + LightlessConfigService configService, + TextureDownscaleService textureDownscaleService, TextureMetadataHelper textureMetadataHelper) : base(logger, mediator) { _downloadStatus = new Dictionary(StringComparer.Ordinal); _orchestrator = orchestrator; _fileDbManager = fileCacheManager; _fileCompactor = fileCompactor; - _pairProcessingLimiter = pairProcessingLimiter; _configService = configService; + _textureDownscaleService = textureDownscaleService; + _textureMetadataHelper = textureMetadataHelper; _activeDownloadStreams = new(); _lastConfigDirectDownloadsState = _configService.Current.EnableDirectDownloads; @@ -63,6 +63,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase public List CurrentDownloads { get; private set; } = []; public List ForbiddenTransfers => _orchestrator.ForbiddenTransfers; + public Guid? CurrentOwnerToken { get; private set; } public bool IsDownloading => CurrentDownloads.Any(); @@ -83,14 +84,15 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase { CurrentDownloads.Clear(); _downloadStatus.Clear(); + CurrentOwnerToken = null; } - public async Task DownloadFiles(GameObjectHandler gameObject, List fileReplacementDto, CancellationToken ct) + public async Task DownloadFiles(GameObjectHandler? gameObject, List fileReplacementDto, CancellationToken ct, bool skipDownscale = false) { Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles))); try { - await DownloadFilesInternal(gameObject, fileReplacementDto, ct).ConfigureAwait(false); + await DownloadFilesInternal(gameObject, fileReplacementDto, ct, skipDownscale).ConfigureAwait(false); } catch { @@ -98,7 +100,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } finally { - Mediator.Publish(new DownloadFinishedMessage(gameObject)); + if (gameObject is not null) + { + Mediator.Publish(new DownloadFinishedMessage(gameObject)); + } Mediator.Publish(new ResumeScanMessage(nameof(DownloadFiles))); } } @@ -272,30 +277,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase int bytesRead; try { - var readTask = stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct).AsTask(); - while (!readTask.IsCompleted) - { - var completedTask = await Task.WhenAny(readTask, Task.Delay(DownloadStallTimeout)).ConfigureAwait(false); - if (completedTask == readTask) - { - break; - } - - ct.ThrowIfCancellationRequested(); - - var snapshot = _pairProcessingLimiter.GetSnapshot(); - if (snapshot.Waiting > 0) - { - throw new TimeoutException($"No data received for {DownloadStallTimeout.TotalSeconds} seconds while downloading {requestUrl} (waiting: {snapshot.Waiting})"); - } - - Logger.LogTrace("Download stalled for {requestUrl} but no queued pairs, continuing to wait", requestUrl); - } - - bytesRead = await readTask.ConfigureAwait(false); + bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct).ConfigureAwait(false); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { + Logger.LogWarning(ex, "Request got cancelled : {url}", requestUrl); throw; } @@ -314,11 +300,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase Logger.LogDebug("{requestUrl} downloaded to {destination}", requestUrl, destinationFilename); } } - catch (TimeoutException ex) - { - Logger.LogWarning(ex, "Detected stalled download for {requestUrl}, aborting transfer", requestUrl); - throw; - } catch (OperationCanceledException) { throw; @@ -352,7 +333,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } } - private async Task DecompressBlockFileAsync(string downloadStatusKey, string blockFilePath, List fileReplacement, string downloadLabel) + private async Task DecompressBlockFileAsync(string downloadStatusKey, string blockFilePath, List fileReplacement, string downloadLabel, bool skipDownscale) { if (_downloadStatus.TryGetValue(downloadStatusKey, out var status)) { @@ -385,7 +366,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase var decompressedFile = LZ4Wrapper.Unwrap(compressedFileContent); await _fileCompactor.WriteAllBytesAsync(filePath, decompressedFile, CancellationToken.None).ConfigureAwait(false); - PersistFileToStorage(fileHash, filePath); + var gamePath = fileReplacement.FirstOrDefault(f => string.Equals(f.Hash, fileHash, StringComparison.OrdinalIgnoreCase))?.GamePaths.FirstOrDefault() ?? string.Empty; + PersistFileToStorage(fileHash, filePath, gamePath, skipDownscale); } catch (EndOfStreamException) { @@ -413,7 +395,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } private async Task PerformDirectDownloadFallbackAsync(DownloadFileTransfer directDownload, List fileReplacement, - IProgress progress, CancellationToken token, bool slotAlreadyAcquired) + IProgress progress, CancellationToken token, bool skipDownscale, bool slotAlreadyAcquired) { if (string.IsNullOrEmpty(directDownload.DirectDownloadUrl)) { @@ -455,7 +437,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase throw new FileNotFoundException("Block file missing after direct download fallback.", blockFile); } - await DecompressBlockFileAsync(downloadKey, blockFile, fileReplacement, $"fallback-{directDownload.Hash}").ConfigureAwait(false); + await DecompressBlockFileAsync(downloadKey, blockFile, fileReplacement, $"fallback-{directDownload.Hash}", skipDownscale).ConfigureAwait(false); } finally { @@ -478,8 +460,9 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } } - public async Task> InitiateDownloadList(GameObjectHandler gameObjectHandler, List fileReplacement, CancellationToken ct) + public async Task> InitiateDownloadList(GameObjectHandler? gameObjectHandler, List fileReplacement, CancellationToken ct, Guid? ownerToken = null) { + CurrentOwnerToken = ownerToken; var objectName = gameObjectHandler?.Name ?? "Unknown"; Logger.LogDebug("Download start: {id}", objectName); @@ -520,7 +503,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase return CurrentDownloads; } - private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List fileReplacement, CancellationToken ct) + private async Task DownloadFilesInternal(GameObjectHandler? gameObjectHandler, List fileReplacement, CancellationToken ct, bool skipDownscale) { var objectName = gameObjectHandler?.Name ?? "Unknown"; @@ -583,7 +566,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase Logger.LogWarning("Downloading {direct} files directly, and {batchtotal} in {batches} batches.", directDownloads.Count, batchDownloads.Count, downloadBatches.Length); } - Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus)); + if (gameObjectHandler is not null) + { + Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus)); + } Task batchDownloadsTask = downloadBatches.Length == 0 ? Task.CompletedTask : Parallel.ForEachAsync(downloadBatches, new ParallelOptions() { @@ -651,7 +637,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase return; } - await DecompressBlockFileAsync(fileGroup.Key, blockFile, fileReplacement, fi.Name).ConfigureAwait(false); + await DecompressBlockFileAsync(fileGroup.Key, blockFile, fileReplacement, fi.Name, skipDownscale).ConfigureAwait(false); } finally { @@ -690,14 +676,13 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase if (!ShouldUseDirectDownloads()) { - await PerformDirectDownloadFallbackAsync(directDownload, fileReplacement, progress, token, slotAlreadyAcquired: false).ConfigureAwait(false); + await PerformDirectDownloadFallbackAsync(directDownload, fileReplacement, progress, token, skipDownscale, slotAlreadyAcquired: false).ConfigureAwait(false); return; } var tempFilename = _fileDbManager.GetCacheFilePath(directDownload.Hash, "bin"); var slotAcquired = false; - try { downloadTracker.DownloadStatus = DownloadStatus.WaitingForSlot; @@ -727,7 +712,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase byte[] compressedBytes = await File.ReadAllBytesAsync(tempFilename).ConfigureAwait(false); var decompressedBytes = LZ4Wrapper.Unwrap(compressedBytes); await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, CancellationToken.None).ConfigureAwait(false); - PersistFileToStorage(directDownload.Hash, finalFilename); + PersistFileToStorage(directDownload.Hash, finalFilename, replacement.GamePaths[0], skipDownscale); downloadTracker.TransferredFiles = 1; Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash); @@ -739,8 +724,15 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } catch (OperationCanceledException ex) { - Logger.LogDebug("{hash}: Detected cancellation of direct download, discarding file.", directDownload.Hash); - Logger.LogError(ex, "{hash}: Error during direct download.", directDownload.Hash); + if (token.IsCancellationRequested) + { + Logger.LogDebug("{hash}: Direct download cancelled by caller, discarding file.", directDownload.Hash); + } + else + { + Logger.LogWarning(ex, "{hash}: Direct download cancelled unexpectedly.", directDownload.Hash); + } + ClearDownload(); return; } @@ -762,7 +754,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase try { downloadTracker.DownloadStatus = DownloadStatus.WaitingForQueue; - await PerformDirectDownloadFallbackAsync(directDownload, fileReplacement, progress, token, slotAcquired).ConfigureAwait(false); + await PerformDirectDownloadFallbackAsync(directDownload, fileReplacement, progress, token, skipDownscale, slotAcquired).ConfigureAwait(false); if (!expectedDirectDownloadFailure && failureCount >= 3 && !_disableDirectDownloads) { @@ -815,7 +807,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase return await response.Content.ReadFromJsonAsync>(cancellationToken: ct).ConfigureAwait(false) ?? []; } - private void PersistFileToStorage(string fileHash, string filePath) + private void PersistFileToStorage(string fileHash, string filePath, string gamePath, bool skipDownscale) { var fi = new FileInfo(filePath); Func RandomDayInThePast() @@ -832,6 +824,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase try { var entry = _fileDbManager.CreateCacheEntry(filePath); + var mapKind = _textureMetadataHelper.DetermineMapKind(gamePath, filePath); + if (!skipDownscale) + { + _textureDownscaleService.ScheduleDownscale(fileHash, filePath, mapKind); + } if (entry != null && !string.Equals(entry.Hash, fileHash, StringComparison.OrdinalIgnoreCase)) { Logger.LogError("Hash mismatch after extracting, got {hash}, expected {expectedHash}, deleting file", entry.Hash, fileHash); diff --git a/LightlessSync/WebAPI/Files/FileTransferOrchestrator.cs b/LightlessSync/WebAPI/Files/FileTransferOrchestrator.cs index de84a81..ac77b23 100644 --- a/LightlessSync/WebAPI/Files/FileTransferOrchestrator.cs +++ b/LightlessSync/WebAPI/Files/FileTransferOrchestrator.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Net.Http.Headers; using System.Net.Http.Json; +using System.Net.Sockets; using System.Reflection; namespace LightlessSync.WebAPI.Files; @@ -84,27 +85,46 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, bool withToken = true) { - using var requestMessage = new HttpRequestMessage(method, uri); - return await SendRequestInternalAsync(requestMessage, ct, httpCompletionOption, withToken).ConfigureAwait(false); + return await SendRequestInternalAsync(() => new HttpRequestMessage(method, uri), + ct, httpCompletionOption, withToken, allowRetry: true).ConfigureAwait(false); } public async Task SendRequestAsync(HttpMethod method, Uri uri, T content, CancellationToken ct, bool withToken = true) where T : class { - using var requestMessage = new HttpRequestMessage(method, uri); - if (content is not ByteArrayContent) - requestMessage.Content = JsonContent.Create(content); - else - requestMessage.Content = content as ByteArrayContent; - return await SendRequestInternalAsync(requestMessage, ct, withToken: withToken).ConfigureAwait(false); + return await SendRequestInternalAsync(() => + { + var requestMessage = new HttpRequestMessage(method, uri); + if (content is not ByteArrayContent byteArrayContent) + { + requestMessage.Content = JsonContent.Create(content); + } + else + { + var clonedContent = new ByteArrayContent(byteArrayContent.ReadAsByteArrayAsync().GetAwaiter().GetResult()); + foreach (var header in byteArrayContent.Headers) + { + clonedContent.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + requestMessage.Content = clonedContent; + } + + return requestMessage; + }, ct, HttpCompletionOption.ResponseContentRead, withToken, + allowRetry: content is not HttpContent || content is ByteArrayContent).ConfigureAwait(false); } public async Task SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content, CancellationToken ct, bool withToken = true) { - using var requestMessage = new HttpRequestMessage(method, uri); - requestMessage.Content = content; - return await SendRequestInternalAsync(requestMessage, ct, withToken: withToken).ConfigureAwait(false); + return await SendRequestInternalAsync(() => + { + var requestMessage = new HttpRequestMessage(method, uri) + { + Content = content + }; + return requestMessage; + }, ct, HttpCompletionOption.ResponseContentRead, withToken, allowRetry: false).ConfigureAwait(false); } public async Task WaitForDownloadSlotAsync(CancellationToken token) @@ -146,39 +166,78 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase return Math.Clamp(dividedLimit, 1, long.MaxValue); } - private async Task SendRequestInternalAsync(HttpRequestMessage requestMessage, - CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, bool withToken = true) + private async Task SendRequestInternalAsync(Func requestFactory, + CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, + bool withToken = true, bool allowRetry = true) { - if (withToken) - { - var token = await _tokenProvider.GetToken().ConfigureAwait(false); - requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); - } + const int maxAttempts = 2; + var attempt = 0; - if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent) + while (true) { - var content = await ((JsonContent)requestMessage.Content).ReadAsStringAsync().ConfigureAwait(false); - Logger.LogDebug("Sending {method} to {uri} (Content: {content})", requestMessage.Method, requestMessage.RequestUri, content); - } - else - { - Logger.LogDebug("Sending {method} to {uri}", requestMessage.Method, requestMessage.RequestUri); - } + attempt++; + using var requestMessage = requestFactory(); - try - { - if (ct != null) - return await _httpClient.SendAsync(requestMessage, httpCompletionOption, ct.Value).ConfigureAwait(false); - return await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false); - } - catch (TaskCanceledException) - { - throw; - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Error during SendRequestInternal for {uri}", requestMessage.RequestUri); - throw; + if (withToken) + { + var token = await _tokenProvider.GetToken().ConfigureAwait(false); + requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + } + + if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent) + { + var content = await ((JsonContent)requestMessage.Content).ReadAsStringAsync().ConfigureAwait(false); + Logger.LogDebug("Sending {method} to {uri} (Content: {content})", requestMessage.Method, requestMessage.RequestUri, content); + } + else + { + Logger.LogDebug("Sending {method} to {uri}", requestMessage.Method, requestMessage.RequestUri); + } + + try + { + if (ct != null) + return await _httpClient.SendAsync(requestMessage, httpCompletionOption, ct.Value).ConfigureAwait(false); + return await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception ex) when (allowRetry && attempt < maxAttempts && IsTransientNetworkException(ex)) + { + Logger.LogWarning(ex, "Transient error during SendRequestInternal for {uri}, retrying attempt {attempt}/{maxAttempts}", + requestMessage.RequestUri, attempt, maxAttempts); + if (ct.HasValue) + { + await Task.Delay(TimeSpan.FromMilliseconds(200), ct.Value).ConfigureAwait(false); + } + else + { + await Task.Delay(TimeSpan.FromMilliseconds(200)).ConfigureAwait(false); + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error during SendRequestInternal for {uri}", requestMessage.RequestUri); + throw; + } } } + + private static bool IsTransientNetworkException(Exception ex) + { + var current = ex; + while (current != null) + { + if (current is SocketException socketEx) + { + return socketEx.SocketErrorCode is SocketError.ConnectionReset or SocketError.ConnectionAborted or SocketError.TimedOut; + } + + current = current.InnerException; + } + + return false; + } } \ No newline at end of file diff --git a/LightlessSync/WebAPI/Files/FileUploadManager.cs b/LightlessSync/WebAPI/Files/FileUploadManager.cs index 09be269..b5db541 100644 --- a/LightlessSync/WebAPI/Files/FileUploadManager.cs +++ b/LightlessSync/WebAPI/Files/FileUploadManager.cs @@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Collections.Concurrent; -using System.Threading; namespace LightlessSync.WebAPI.Files; @@ -44,6 +43,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase } public List CurrentUploads { get; } = []; + public bool IsReady => _orchestrator.IsInitialized; public bool IsUploading { get diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index a4c78f8..2f317b9 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -1,5 +1,6 @@ -using LightlessSync.API.Data; +using LightlessSync.API.Data; using LightlessSync.API.Dto; +using LightlessSync.API.Dto.Chat; using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using Microsoft.AspNetCore.SignalR.Client; @@ -41,6 +42,29 @@ public partial class ApiController await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid).ConfigureAwait(false); } + public async Task UpdateChatPresence(ChatPresenceUpdateDto presence) + { + if (!IsConnected || _lightlessHub is null) return; + await _lightlessHub.InvokeAsync(nameof(UpdateChatPresence), presence).ConfigureAwait(false); + } + + public async Task SendChatMessage(ChatSendRequestDto request) + { + if (!IsConnected || _lightlessHub is null) return; + await _lightlessHub.InvokeAsync(nameof(SendChatMessage), request).ConfigureAwait(false); + } + + public async Task ReportChatMessage(ChatReportSubmitDto request) + { + if (!IsConnected || _lightlessHub is null) return; + await _lightlessHub.InvokeAsync(nameof(ReportChatMessage), request).ConfigureAwait(false); + } + + public async Task SetChatParticipantMute(ChatParticipantMuteRequestDto request) + { + if (!IsConnected || _lightlessHub is null) return; + await _lightlessHub.InvokeAsync(nameof(SetChatParticipantMute), request).ConfigureAwait(false); + } public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null) { CheckConnection(); @@ -88,6 +112,12 @@ public partial class ApiController return await _lightlessHub!.InvokeAsync(nameof(UserGetProfile), dto).ConfigureAwait(false); } + public async Task UserGetLightfinderProfile(string hashedCid) + { + if (!IsConnected) return null; + return await _lightlessHub!.InvokeAsync(nameof(UserGetLightfinderProfile), hashedCid).ConfigureAwait(false); + } + public async Task UserPushData(UserCharaDataMessageDto dto) { try diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs index 8323fc3..490800f 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -1,7 +1,9 @@ +using System; using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.API.Dto; using LightlessSync.API.Dto.CharaData; +using LightlessSync.API.Dto.Chat; using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSync.LightlessConfiguration.Models; @@ -24,21 +26,21 @@ public partial class ApiController public Task Client_GroupChangePermissions(GroupPermissionDto groupPermission) { Logger.LogTrace("Client_GroupChangePermissions: {perm}", groupPermission); - ExecuteSafely(() => _pairManager.SetGroupPermissions(groupPermission)); + ExecuteSafely(() => _pairCoordinator.HandleGroupChangePermissions(groupPermission)); return Task.CompletedTask; } public Task Client_GroupChangeUserPairPermissions(GroupPairUserPermissionDto dto) { Logger.LogDebug("Client_GroupChangeUserPairPermissions: {dto}", dto); - ExecuteSafely(() => _pairManager.UpdateGroupPairPermissions(dto)); + ExecuteSafely(() => _pairCoordinator.HandleGroupPairPermissions(dto)); return Task.CompletedTask; } public Task Client_GroupDelete(GroupDto groupDto) { Logger.LogTrace("Client_GroupDelete: {dto}", groupDto); - ExecuteSafely(() => _pairManager.RemoveGroup(groupDto.Group)); + ExecuteSafely(() => _pairCoordinator.HandleGroupRemoved(groupDto)); return Task.CompletedTask; } @@ -47,8 +49,8 @@ public partial class ApiController Logger.LogTrace("Client_GroupPairChangeUserInfo: {dto}", userInfo); ExecuteSafely(() => { - if (string.Equals(userInfo.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupStatusInfo(userInfo); - else _pairManager.SetGroupPairStatusInfo(userInfo); + var isSelf = string.Equals(userInfo.UID, UID, StringComparison.Ordinal); + _pairCoordinator.HandleGroupPairStatus(userInfo, isSelf); }); return Task.CompletedTask; } @@ -56,28 +58,28 @@ public partial class ApiController public Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto) { Logger.LogTrace("Client_GroupPairJoined: {dto}", groupPairInfoDto); - ExecuteSafely(() => _pairManager.AddGroupPair(groupPairInfoDto)); + ExecuteSafely(() => _pairCoordinator.HandleGroupPairJoined(groupPairInfoDto)); return Task.CompletedTask; } public Task Client_GroupPairLeft(GroupPairDto groupPairDto) { Logger.LogTrace("Client_GroupPairLeft: {dto}", groupPairDto); - ExecuteSafely(() => _pairManager.RemoveGroupPair(groupPairDto)); + ExecuteSafely(() => _pairCoordinator.HandleGroupPairLeft(groupPairDto)); return Task.CompletedTask; } public Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo) { Logger.LogTrace("Client_GroupSendFullInfo: {dto}", groupInfo); - ExecuteSafely(() => _pairManager.AddGroup(groupInfo)); + ExecuteSafely(() => _pairCoordinator.HandleGroupFullInfo(groupInfo)); return Task.CompletedTask; } public Task Client_GroupSendInfo(GroupInfoDto groupInfo) { Logger.LogTrace("Client_GroupSendInfo: {dto}", groupInfo); - ExecuteSafely(() => _pairManager.SetGroupInfo(groupInfo)); + ExecuteSafely(() => _pairCoordinator.HandleGroupInfoUpdate(groupInfo)); return Task.CompletedTask; } @@ -129,52 +131,62 @@ public partial class ApiController return Task.CompletedTask; } + public Task Client_ChatReceive(ChatMessageDto message) + { + Logger.LogTrace("Client_ChatReceive: {@channel}", message.Channel); + ExecuteSafely(() => ChatMessageReceived?.Invoke(message)); + return Task.CompletedTask; + } + public Task Client_UpdateUserIndividualPairStatusDto(UserIndividualPairStatusDto dto) { Logger.LogDebug("Client_UpdateUserIndividualPairStatusDto: {dto}", dto); - ExecuteSafely(() => _pairManager.UpdateIndividualPairStatus(dto)); + ExecuteSafely(() => _pairCoordinator.HandleUserStatus(dto)); return Task.CompletedTask; } public Task Client_UserAddClientPair(UserPairDto dto) { Logger.LogDebug("Client_UserAddClientPair: {dto}", dto); - ExecuteSafely(() => _pairManager.AddUserPair(dto, addToLastAddedUser: true)); + ExecuteSafely(() => _pairCoordinator.HandleUserAddPair(dto, addToLastAddedUser: true)); return Task.CompletedTask; } public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto) { Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User); - ExecuteSafely(() => _pairManager.ReceiveCharaData(dataDto)); + ExecuteSafely(() => _pairCoordinator.HandleCharacterData(dataDto)); return Task.CompletedTask; } public Task Client_UserReceiveUploadStatus(UserDto dto) { Logger.LogTrace("Client_UserReceiveUploadStatus: {dto}", dto); - ExecuteSafely(() => _pairManager.ReceiveUploadStatus(dto)); + ExecuteSafely(() => + { + _pairCoordinator.HandleUploadStatus(dto); + }); return Task.CompletedTask; } public Task Client_UserRemoveClientPair(UserDto dto) { Logger.LogDebug("Client_UserRemoveClientPair: {dto}", dto); - ExecuteSafely(() => _pairManager.RemoveUserPair(dto)); + ExecuteSafely(() => _pairCoordinator.HandleUserRemovePair(dto)); return Task.CompletedTask; } public Task Client_UserSendOffline(UserDto dto) { Logger.LogDebug("Client_UserSendOffline: {dto}", dto); - ExecuteSafely(() => _pairManager.MarkPairOffline(dto.User)); + ExecuteSafely(() => _pairCoordinator.HandleUserOffline(dto.User)); return Task.CompletedTask; } public Task Client_UserSendOnline(OnlineUserIdentDto dto) { Logger.LogDebug("Client_UserSendOnline: {dto}", dto); - ExecuteSafely(() => _pairManager.MarkPairOnline(dto)); + ExecuteSafely(() => _pairCoordinator.HandleUserOnline(dto, sendNotification: true)); return Task.CompletedTask; } @@ -188,7 +200,7 @@ public partial class ApiController public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto) { Logger.LogDebug("Client_UserUpdateOtherPairPermissions: {dto}", dto); - ExecuteSafely(() => _pairManager.UpdatePairPermissions(dto)); + ExecuteSafely(() => _pairCoordinator.HandleUserPermissions(dto)); return Task.CompletedTask; } @@ -209,7 +221,7 @@ public partial class ApiController public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto) { Logger.LogDebug("Client_UserUpdateSelfPairPermissions: {dto}", dto); - ExecuteSafely(() => _pairManager.UpdateSelfPairPermissions(dto)); + ExecuteSafely(() => _pairCoordinator.HandleSelfPermissions(dto)); return Task.CompletedTask; } diff --git a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs index d212f6c..88264b9 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.Functions.Groups.cs @@ -151,6 +151,20 @@ public partial class ApiController .ConfigureAwait(false); } + public async Task GroupGetPruneSettings(GroupDto dto) + { + CheckConnection(); + return await _lightlessHub!.InvokeAsync(nameof(GroupGetPruneSettings), dto) + .ConfigureAwait(false); + } + + public async Task GroupSetPruneSettings(GroupPruneSettingsDto dto) + { + CheckConnection(); + await _lightlessHub!.SendAsync(nameof(GroupSetPruneSettings), dto) + .ConfigureAwait(false); + } + private void CheckConnection() { if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected"); diff --git a/LightlessSync/WebAPI/SignalR/ApiController.cs b/LightlessSync/WebAPI/SignalR/ApiController.cs index d2fddc5..011a6d8 100644 --- a/LightlessSync/WebAPI/SignalR/ApiController.cs +++ b/LightlessSync/WebAPI/SignalR/ApiController.cs @@ -1,9 +1,9 @@ +using System.Reflection; using Dalamud.Utility; using LightlessSync.API.Data; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto; using LightlessSync.API.Dto.Chat; -using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.User; using LightlessSync.API.SignalR; using LightlessSync.LightlessConfiguration; @@ -16,7 +16,6 @@ using LightlessSync.WebAPI.SignalR; using LightlessSync.WebAPI.SignalR.Utils; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; -using System.Reflection; namespace LightlessSync.WebAPI; @@ -28,7 +27,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private readonly DalamudUtilService _dalamudUtil; private readonly HubFactory _hubFactory; - private readonly PairManager _pairManager; + private readonly PairCoordinator _pairCoordinator; private readonly PairRequestService _pairRequestService; private readonly ServerConfigurationManager _serverManager; private readonly TokenProvider _tokenProvider; @@ -42,14 +41,17 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private HubConnection? _lightlessHub; private ServerState _serverState; private CensusUpdateMessage? _lastCensus; + private IReadOnlyList _zoneChatChannels = Array.Empty(); + private IReadOnlyList _groupChatChannels = Array.Empty(); + private event Action? ChatMessageReceived; public ApiController(ILogger logger, HubFactory hubFactory, DalamudUtilService dalamudUtil, - PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator, + PairCoordinator pairCoordinator, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator, TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, NotificationService lightlessNotificationService) : base(logger, mediator) { _hubFactory = hubFactory; _dalamudUtil = dalamudUtil; - _pairManager = pairManager; + _pairCoordinator = pairCoordinator; _pairRequestService = pairRequestService; _serverManager = serverManager; _tokenProvider = tokenProvider; @@ -61,7 +63,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Mediator.Subscribe(this, (msg) => LightlessHubOnClosed(msg.Exception)); Mediator.Subscribe(this, (msg) => _ = LightlessHubOnReconnectedAsync()); Mediator.Subscribe(this, (msg) => LightlessHubOnReconnecting(msg.Exception)); - Mediator.Subscribe(this, (msg) => _ = CyclePauseAsync(msg.UserData)); + Mediator.Subscribe(this, (msg) => _ = CyclePauseAsync(msg.Pair)); Mediator.Subscribe(this, (msg) => _lastCensus = msg); Mediator.Subscribe(this, (msg) => _ = PauseAsync(msg.UserData)); @@ -106,15 +108,65 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL public SystemInfoDto SystemInfoDto { get; private set; } = new(); + public IReadOnlyList ZoneChatChannels => _zoneChatChannels; + public IReadOnlyList GroupChatChannels => _groupChatChannels; public string UID => _connectionDto?.User.UID ?? string.Empty; public event Action? OnConnected; public async Task CheckClientHealth() { - return await _lightlessHub!.InvokeAsync(nameof(CheckClientHealth)).ConfigureAwait(false); + var hub = _lightlessHub; + if (hub is null || !IsConnected) + { + return false; + } + + try + { + return await hub.InvokeAsync(nameof(CheckClientHealth)).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Client health check failed."); + return false; + } } + public async Task RefreshChatChannelsAsync() + { + if (_lightlessHub is null || !IsConnected) + return; + + await Task.WhenAll(GetZoneChatChannelsAsync(), GetGroupChatChannelsAsync()).ConfigureAwait(false); + } + + public async Task> GetZoneChatChannelsAsync() + { + if (_lightlessHub is null || !IsConnected) + return _zoneChatChannels; + + var channels = await _lightlessHub.InvokeAsync>("GetZoneChatChannels").ConfigureAwait(false); + _zoneChatChannels = channels; + return channels; + } + + public async Task> GetGroupChatChannelsAsync() + { + if (_lightlessHub is null || !IsConnected) + return _groupChatChannels; + + var channels = await _lightlessHub.InvokeAsync>("GetGroupChatChannels").ConfigureAwait(false); + _groupChatChannels = channels; + return channels; + } + + Task> ILightlessHub.GetZoneChatChannels() + => _lightlessHub!.InvokeAsync>("GetZoneChatChannels"); + + Task> ILightlessHub.GetGroupChatChannels() + => _lightlessHub!.InvokeAsync>("GetGroupChatChannels"); + public async Task CreateConnectionsAsync() { if (!_serverManager.ShownCensusPopup) @@ -133,7 +185,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Logger.LogInformation("Not recreating Connection, paused"); _connectionDto = null; await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } return; } @@ -147,7 +202,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Lightless.", NotificationType.Error)); await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } return; } @@ -156,7 +214,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Logger.LogWarning("No secret key set for current character"); _connectionDto = null; await StopConnectionAsync(ServerState.NoSecretKey).ConfigureAwait(false); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } return; } } @@ -170,7 +231,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Lightless.", NotificationType.Error)); await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } return; } @@ -179,7 +243,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Logger.LogWarning("No UID/OAuth set for current character"); _connectionDto = null; await StopConnectionAsync(ServerState.OAuthMisconfigured).ConfigureAwait(false); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } return; } @@ -188,7 +255,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Logger.LogWarning("OAuth2 login token could not be updated"); _connectionDto = null; await StopConnectionAsync(ServerState.OAuthLoginTokenStale).ConfigureAwait(false); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } return; } } @@ -199,7 +269,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Informational, $"Starting Connection to {_serverManager.CurrentServer.ServerName}"))); - _connectionCancellationTokenSource?.Cancel(); + if (_connectionCancellationTokenSource != null) + { + await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false); + } _connectionCancellationTokenSource?.Dispose(); _connectionCancellationTokenSource = new CancellationTokenSource(); var token = _connectionCancellationTokenSource.Token; @@ -337,35 +410,86 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private bool _naggedAboutLod = false; - public Task CyclePauseAsync(UserData userData) + public Task CyclePauseAsync(Pair pair) { - CancellationTokenSource cts = new(); - cts.CancelAfter(TimeSpan.FromSeconds(5)); + ArgumentNullException.ThrowIfNull(pair); + return CyclePauseAsync(pair.UniqueIdent); + } + + public Task CyclePauseAsync(PairUniqueIdentifier ident) + { + var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(8)); _ = Task.Run(async () => { - var pair = _pairManager.GetOnlineUserPairs().Single(p => p.UserPair != null && p.UserData == userData); - var perm = pair.UserPair!.OwnPermissions; - perm.SetPaused(paused: true); - await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false); - // wait until it's changed - while (pair.UserPair!.OwnPermissions != perm) + var token = timeoutCts.Token; + try { - await Task.Delay(250, cts.Token).ConfigureAwait(false); - Logger.LogTrace("Waiting for permissions change for {data}", userData); + if (!_pairCoordinator.Ledger.TryGetEntry(ident, out var entry) || entry is null) + { + Logger.LogWarning("CyclePauseAsync: pair {uid} not found in ledger", ident.UserId); + return; + } + + var originalPermissions = entry.SelfPermissions; + var targetPermissions = originalPermissions; + targetPermissions.SetPaused(!originalPermissions.IsPaused()); + + await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false); + + var applied = false; + while (!token.IsCancellationRequested) + { + if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null) + { + if (updated.SelfPermissions == targetPermissions) + { + applied = true; + entry = updated; + break; + } + } + + await Task.Delay(250, token).ConfigureAwait(false); + Logger.LogTrace("Waiting for permissions change for {uid}", ident.UserId); + } + + if (!applied) + { + Logger.LogWarning("CyclePauseAsync timed out waiting for pause acknowledgement for {uid}", ident.UserId); + return; + } + + Logger.LogDebug("CyclePauseAsync toggled paused for {uid} to {state}", ident.UserId, targetPermissions.IsPaused()); } - perm.SetPaused(paused: false); - await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false); - }, cts.Token).ContinueWith((t) => cts.Dispose()); + catch (OperationCanceledException) + { + Logger.LogDebug("CyclePauseAsync cancelled for {uid}", ident.UserId); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "CyclePauseAsync failed for {uid}", ident.UserId); + } + finally + { + timeoutCts.Dispose(); + } + }, CancellationToken.None); return Task.CompletedTask; } public async Task PauseAsync(UserData userData) { - var pair = _pairManager.GetOnlineUserPairs().Single(p => p.UserPair != null && p.UserData == userData); - var perm = pair.UserPair!.OwnPermissions; - perm.SetPaused(paused: true); - await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false); + var pairIdent = new PairUniqueIdentifier(userData.UID); + if (!_pairCoordinator.Ledger.TryGetEntry(pairIdent, out var entry) || entry is null) + { + Logger.LogWarning("PauseAsync: pair {uid} not found in ledger", userData.UID); + return; + } + + var permissions = entry.SelfPermissions; + permissions.SetPaused(paused: true); + await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false); } public Task GetConnectionDto() => GetConnectionDtoAsync(true); @@ -388,8 +512,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL private async Task ClientHealthCheckAsync(CancellationToken ct) { - while (!ct.IsCancellationRequested && _lightlessHub != null) + while (!ct.IsCancellationRequested) { + if (_lightlessHub is null) + { + break; + } + await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false); Logger.LogDebug("Checking Client Health State"); @@ -455,6 +584,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto)); OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto)); OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto)); + if (!_initialized) + { + _lightlessHub.On(nameof(Client_ChatReceive), (Func)Client_ChatReceive); + } OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto)); OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto)); @@ -470,18 +603,36 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL _initialized = true; } + private readonly HashSet> _chatHandlers = new(); + + public void RegisterChatMessageHandler(Action handler) + { + if (_chatHandlers.Add(handler)) + { + ChatMessageReceived += handler; + } + } + + public void UnregisterChatMessageHandler(Action handler) + { + if (_chatHandlers.Remove(handler)) + { + ChatMessageReceived -= handler; + } + } + private async Task LoadIninitialPairsAsync() { foreach (var entry in await GroupsGetAll().ConfigureAwait(false)) { Logger.LogDebug("Group: {entry}", entry); - _pairManager.AddGroup(entry); + _pairCoordinator.HandleGroupFullInfo(entry); } foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false)) { Logger.LogDebug("Individual Pair: {userPair}", userPair); - _pairManager.AddUserPair(userPair); + _pairCoordinator.HandleUserAddPair(userPair); } } @@ -498,7 +649,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL foreach (var entry in await UserGetOnlinePairs(dto).ConfigureAwait(false)) { Logger.LogDebug("Pair online: {pair}", entry); - _pairManager.MarkPairOnline(entry, sendNotif: false); + _pairCoordinator.HandleUserOnline(entry, sendNotification: false); } } @@ -598,53 +749,18 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL $"Stopping existing connection to {_serverManager.CurrentServer.ServerName}"))); _initialized = false; - _healthCheckTokenSource?.Cancel(); + if (_healthCheckTokenSource != null) + { + await _healthCheckTokenSource.CancelAsync().ConfigureAwait(false); + } Mediator.Publish(new DisconnectedMessage()); _lightlessHub = null; _connectionDto = null; + _zoneChatChannels = Array.Empty(); + _groupChatChannels = Array.Empty(); } ServerState = state; } - - public Task UserGetLightfinderProfile(string hashedCid) - { - throw new NotImplementedException(); - } - - public Task UpdateChatPresence(ChatPresenceUpdateDto presence) - { - throw new NotImplementedException(); - } - - public Task Client_ChatReceive(ChatMessageDto message) - { - throw new NotImplementedException(); - } - - public Task> GetZoneChatChannels() - { - throw new NotImplementedException(); - } - - public Task> GetGroupChatChannels() - { - throw new NotImplementedException(); - } - - public Task SendChatMessage(ChatSendRequestDto request) - { - throw new NotImplementedException(); - } - - public Task ReportChatMessage(ChatReportSubmitDto request) - { - throw new NotImplementedException(); - } - - public Task ResolveChatParticipant(ChatParticipantResolveRequestDto request) - { - throw new NotImplementedException(); - } } -#pragma warning restore MA0040 \ No newline at end of file +#pragma warning restore MA0040 diff --git a/LightlessSync/WebAPI/SignalR/HubFactory.cs b/LightlessSync/WebAPI/SignalR/HubFactory.cs index 1d5a0c8..9b008f0 100644 --- a/LightlessSync/WebAPI/SignalR/HubFactory.cs +++ b/LightlessSync/WebAPI/SignalR/HubFactory.cs @@ -71,6 +71,7 @@ public class HubFactory : MediatorSubscriberBase }; Logger.LogDebug("Building new HubConnection using transport {transport}", transportType); + var msgpackOptions = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block).WithResolver(ContractlessStandardResolver.Instance); _instance = new HubConnectionBuilder() .WithUrl(_serverConfigurationManager.CurrentApiUrl + ILightlessHub.Path, options => @@ -80,22 +81,7 @@ public class HubFactory : MediatorSubscriberBase }) .AddMessagePackProtocol(opt => { - var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance, - BuiltinResolver.Instance, - AttributeFormatterResolver.Instance, - // replace enum resolver - DynamicEnumAsStringResolver.Instance, - DynamicGenericResolver.Instance, - DynamicUnionResolver.Instance, - DynamicObjectResolver.Instance, - PrimitiveObjectResolver.Instance, - // final fallback(last priority) - StandardResolver.Instance); - - opt.SerializerOptions = - MessagePackSerializerOptions.Standard - .WithCompression(MessagePackCompression.Lz4Block) - .WithResolver(resolver); + opt.SerializerOptions = msgpackOptions; }) .WithAutomaticReconnect(new ForeverRetryPolicy(Mediator)) .ConfigureLogging(a => diff --git a/LightlessSync/lib/DirectXTexC.dll b/LightlessSync/lib/DirectXTexC.dll new file mode 100644 index 0000000..2cab1dc Binary files /dev/null and b/LightlessSync/lib/DirectXTexC.dll differ diff --git a/LightlessSync/lib/OtterTex.dll b/LightlessSync/lib/OtterTex.dll new file mode 100644 index 0000000..c137aee Binary files /dev/null and b/LightlessSync/lib/OtterTex.dll differ diff --git a/LightlessSync/packages.lock.json b/LightlessSync/packages.lock.json index a7576db..d47880c 100644 --- a/LightlessSync/packages.lock.json +++ b/LightlessSync/packages.lock.json @@ -1,18 +1,30 @@ { "version": 1, "dependencies": { - "net9.0-windows7.0": { + "net10.0-windows7.0": { + "Blake3": { + "type": "Direct", + "requested": "[2.0.0, )", + "resolved": "2.0.0", + "contentHash": "v447kojeuNYSY5dvtVGG2bv1+M3vOWJXcrYWwXho/2uUpuwK6qPeu5WSMlqLm4VRJu96kysVO11La0zN3dLAuQ==" + }, + "Brio.API": { + "type": "Direct", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "40MD49ETqyGsdHGoG3JF/BFcNAphRqi27+ZxfDk2Aj7gAkzDFe7C2UVGirUByrUIj8lxiz9eEoB2i7O9lefEPQ==" + }, "DalamudPackager": { "type": "Direct", - "requested": "[13.1.0, )", - "resolved": "13.1.0", - "contentHash": "XdoNhJGyFby5M/sdcRhnc5xTop9PHy+H50PTWpzLhJugjB19EDBiHD/AsiDF66RETM+0qKUdJBZrNuebn7qswQ==" + "requested": "[14.0.1, )", + "resolved": "14.0.1", + "contentHash": "y0WWyUE6dhpGdolK3iKgwys05/nZaVf4ZPtIjpLhJBZvHxkkiE23zYRo7K7uqAgoK/QvK5cqF6l3VG5AbgC6KA==" }, "DotNet.ReproducibleBuilds": { "type": "Direct", - "requested": "[1.2.25, )", - "resolved": "1.2.25", - "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + "requested": "[1.2.39, )", + "resolved": "1.2.39", + "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" }, "Downloader": { "type": "Direct", @@ -25,9 +37,9 @@ }, "Glamourer.Api": { "type": "Direct", - "requested": "[2.6.0, )", - "resolved": "2.6.0", - "contentHash": "zysCZgNBRm3k3qvibyw/31MmEckX0Uh0ZsT+Sax3ZHnYIRELr9Qhbz3cjJz7u0RHGIrNJiRpktu/LxgHEqDItw==" + "requested": "[2.8.0, )", + "resolved": "2.8.0", + "contentHash": "dCxycU+lA0qraE70ZoRvM4GQAPq/K+qL/bg6t/kxKPox5GWaiunKOTXNOG2hOvgEda5WtFy6e3c9OuIM6L3faQ==" }, "K4os.Compression.LZ4.Legacy": { "type": "Direct", @@ -40,97 +52,91 @@ }, "Meziantou.Analyzer": { "type": "Direct", - "requested": "[2.0.212, )", - "resolved": "2.0.212", - "contentHash": "U91ktjjTRTccUs3Lk+hrLD9vW+2+lhnsOf4G1GpRSJi1pLn3uK5CU6wGP9Bmz1KlJs6Oz1GGoMhxQBoqQsmAuQ==" + "requested": "[2.0.264, )", + "resolved": "2.0.264", + "contentHash": "zRG13RDG446rZNdd/YjKRd4utpbjleRDUqNQSrX0etMnH8Rz9NBlXUpS5aR2ExoOokhNfkdOW8HpLzjLj5x0hQ==" }, "Microsoft.AspNetCore.SignalR.Client": { "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "V8K94AN9ADbpP2jxwt8Y++g7t/XZ7oEV+GZizNvLnR8dpCYWeveIZ/tItO54jfZJ5jmt5YyideOc+ErZbr1IZg==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "mGlS8W2siQaJVVId2VGQ0I+7Lj49oqFxsb/bIil7GBNeazB6fBP8Ljf5tZUNzUN9WdQU5aI85WXCW9+Fsx2dZQ==", "dependencies": { - "Microsoft.AspNetCore.Http.Connections.Client": "9.0.3", - "Microsoft.AspNetCore.SignalR.Client.Core": "9.0.3" + "Microsoft.AspNetCore.Http.Connections.Client": "10.0.1", + "Microsoft.AspNetCore.SignalR.Client.Core": "10.0.1" } }, "Microsoft.AspNetCore.SignalR.Protocols.MessagePack": { "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "mMQ21T4NuqGrX1UzSe1WBmg6TUlOmpMgCoA9kAy/uBWBZlAA4+NFavbCULyJy6zTSUAvZkG3cGSnQN4dLJlF/w==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "WWrnA6CosCDfMKgLkmH/65c+7XV1js4FXl8Ft/Izshn3O+r8cQkT64Om7VcVy+pa6nlSt32tY5TjV/jT/84tkQ==", "dependencies": { "MessagePack": "2.5.187", - "Microsoft.AspNetCore.SignalR.Common": "9.0.3" + "Microsoft.AspNetCore.SignalR.Common": "10.0.1" } }, "Microsoft.Extensions.Hosting": { "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "ioFXglqFA9uCYcKHI3CLVTO3I75jWIhvVxiZBzGeSPxw7XdhDLh0QvbNFrMTbZk9qqEVQcylblcvcNXnFHYXyA==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "0jjfjQSOFZlHhwOoIQw0WyzxtkDMYdnPY3iFrOLasxYq/5J4FDt1HWT8TktBclOVjFY1FOOkoOc99X7AhbqSIw==", "dependencies": { - "Microsoft.Extensions.Configuration": "9.0.3", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", - "Microsoft.Extensions.Configuration.Binder": "9.0.3", - "Microsoft.Extensions.Configuration.CommandLine": "9.0.3", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "9.0.3", - "Microsoft.Extensions.Configuration.FileExtensions": "9.0.3", - "Microsoft.Extensions.Configuration.Json": "9.0.3", - "Microsoft.Extensions.Configuration.UserSecrets": "9.0.3", - "Microsoft.Extensions.DependencyInjection": "9.0.3", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", - "Microsoft.Extensions.Diagnostics": "9.0.3", - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.3", - "Microsoft.Extensions.FileProviders.Physical": "9.0.3", - "Microsoft.Extensions.Hosting.Abstractions": "9.0.3", - "Microsoft.Extensions.Logging": "9.0.3", - "Microsoft.Extensions.Logging.Abstractions": "9.0.3", - "Microsoft.Extensions.Logging.Configuration": "9.0.3", - "Microsoft.Extensions.Logging.Console": "9.0.3", - "Microsoft.Extensions.Logging.Debug": "9.0.3", - "Microsoft.Extensions.Logging.EventLog": "9.0.3", - "Microsoft.Extensions.Logging.EventSource": "9.0.3", - "Microsoft.Extensions.Options": "9.0.3" + "Microsoft.Extensions.Configuration": "10.0.1", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", + "Microsoft.Extensions.Configuration.Binder": "10.0.1", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.1", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.1", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.1", + "Microsoft.Extensions.Configuration.Json": "10.0.1", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.1", + "Microsoft.Extensions.DependencyInjection": "10.0.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", + "Microsoft.Extensions.Diagnostics": "10.0.1", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.1", + "Microsoft.Extensions.FileProviders.Physical": "10.0.1", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.1", + "Microsoft.Extensions.Logging": "10.0.1", + "Microsoft.Extensions.Logging.Abstractions": "10.0.1", + "Microsoft.Extensions.Logging.Configuration": "10.0.1", + "Microsoft.Extensions.Logging.Console": "10.0.1", + "Microsoft.Extensions.Logging.Debug": "10.0.1", + "Microsoft.Extensions.Logging.EventLog": "10.0.1", + "Microsoft.Extensions.Logging.EventSource": "10.0.1", + "Microsoft.Extensions.Options": "10.0.1" } }, "NReco.Logging.File": { "type": "Direct", - "requested": "[1.2.2, )", - "resolved": "1.2.2", - "contentHash": "UyUIkyDiHi2HAJlmEWqeKN9/FxTF0DPNdyatzMDMTXvUpgvqBFneJ2qDtZkXRJNG8eR6jU+KsbGeMmChgUdRUg==", + "requested": "[1.3.1, )", + "resolved": "1.3.1", + "contentHash": "4aFUEW1OFJsuKtg46dnqxZUyb37f9dzaWOXjUv2x/wzoHKovR9yqiMzXtCZt3+a9G78YCIAtSEz2g/GaNYbxSQ==", "dependencies": { - "Microsoft.Extensions.Logging": "8.0.1", - "Microsoft.Extensions.Logging.Configuration": "8.0.1", - "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + "Microsoft.Extensions.Logging": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.0" } }, - "Penumbra.String": { - "type": "Direct", - "requested": "[1.0.5, )", - "resolved": "1.0.5", - "contentHash": "+9YRQxwkzW6Ys/hx8vHvTwYV76QMjbf7Puq5SibxVLNzkPLyKLp7qZCKS1SC4yXPJlPB4g80IqxrxCm0yacMFw==" - }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[3.1.11, )", - "resolved": "3.1.11", - "contentHash": "JfPLyigLthuE50yi6tMt7Amrenr/fA31t2CvJyhy/kQmfulIBAqo5T/YFUSRHtuYPXRSaUHygFeh6Qd933EoSw==" + "requested": "[3.1.12, )", + "resolved": "3.1.12", + "contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==" }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.7.0.110445, )", - "resolved": "10.7.0.110445", - "contentHash": "U4v2LWopxADYkUv7Z5CX7ifKMdDVqHb7a1bzppIQnQi4WQR6z1Zi5rDkCHlVYGEd1U/WMz1IJCU8OmFZLJpVig==" + "requested": "[10.17.0.131074, )", + "resolved": "10.17.0.131074", + "contentHash": "N8agHzX1pK3Xv/fqMig/mHspPAmh/aKkGg7lUC1xfezAhFtPTuRqBjuyas622Tvy5jnsN5zCXJVclvNkfJJ4rQ==" }, "System.IdentityModel.Tokens.Jwt": { "type": "Direct", - "requested": "[8.7.0, )", - "resolved": "8.7.0", - "contentHash": "8dKL3A9pVqYCJIXHd4H2epQqLxSvKeNxGonR0e5g89yMchyvsM/NLuB06otx29BicUd6+LUJZgNZmvYjjPsPGg==", + "requested": "[8.15.0, )", + "resolved": "8.15.0", + "contentHash": "dpodi7ixz6hxK8YCBYAWzm0IA8JYXoKcz0hbCbNifo519//rjUI0fBD8rfNr+IGqq+2gm4oQoXwHk09LX5SqqQ==", "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "8.7.0", - "Microsoft.IdentityModel.Tokens": "8.7.0" + "Microsoft.IdentityModel.JsonWebTokens": "8.15.0", + "Microsoft.IdentityModel.Tokens": "8.15.0" } }, "YamlDotNet": { @@ -139,6 +145,21 @@ "resolved": "16.3.0", "contentHash": "SgMOdxbz8X65z8hraIs6hOEdnkH6hESTAIUa7viEngHOYaH+6q5XJmwr1+yb9vJpNQ19hCQY69xbFsLtXpobQA==" }, + "FlatSharp.Compiler": { + "type": "Transitive", + "resolved": "7.9.0", + "contentHash": "MU6808xvdbWJ3Ev+5PKalqQuzvVbn1DzzQH8txRDHGFUNDvHjd+ejqpvnYc9BSJ8Qp8VjkkpJD8OzRYilbPp3A==" + }, + "FlatSharp.Runtime": { + "type": "Transitive", + "resolved": "7.9.0", + "contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "2024.3.0", + "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" + }, "K4os.Compression.LZ4": { "type": "Transitive", "resolved": "1.3.8", @@ -160,342 +181,341 @@ }, "Microsoft.AspNetCore.Connections.Abstractions": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "MWkNy/Yhv2q5ZVYPHjHN6pEE5Ya1r4opqSpnsW60bgpDOT54zZ6Kpqub4Tcat8ENsR5PZcTZ3eeSAthweUb/KA==", + "resolved": "10.0.1", + "contentHash": "/jLwhtGfKPbXK395evmQYhBObZ9sZ7pckirDBTwpSM6QSJGXbUakzviOo84OmfaKj36btwfR/uaKu1hNlssUAA==", "dependencies": { - "Microsoft.Extensions.Features": "9.0.3" + "Microsoft.Extensions.Features": "10.0.1" } }, "Microsoft.AspNetCore.Http.Connections.Client": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "bLoLX67FBeYK1KKGfXrmBki/F9EAK8EKCNkyADtfFjQkJ1Qhhw1sjBlcL8TbVnZxk+FaFsyCeBPmSHgOwNIJ/A==", + "resolved": "10.0.1", + "contentHash": "+6lrifIZCL1heJtLugtkqEa191BIfUkhyAnBORbHg1eg4Vl+ijsxAzyOZxQTZbVMSJHKQCQFTEKf6H+YpSDWjA==", "dependencies": { - "Microsoft.AspNetCore.Http.Connections.Common": "9.0.3", - "Microsoft.Extensions.Logging.Abstractions": "9.0.3", - "Microsoft.Extensions.Options": "9.0.3", - "System.Net.ServerSentEvents": "9.0.3" + "Microsoft.AspNetCore.Http.Connections.Common": "10.0.1", + "Microsoft.Extensions.Logging.Abstractions": "10.0.1", + "Microsoft.Extensions.Options": "10.0.1" } }, "Microsoft.AspNetCore.Http.Connections.Common": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "GYDAXEmaG/q9UgixPchsLAVbBUbdgG3hd8J7Af4k4GIKLsibAhos7QY7hHicyULJvRtl03totiRi5Z+JIKEnUA==", + "resolved": "10.0.1", + "contentHash": "RzryafnXWvWncojw6vD15tVdbhe3LE7MiosmLpJ5AqcWyWLk5oBACtzpp7fU5Yqa8Zc3Pcbe3jXu5DRMCRm6Xw==", "dependencies": { - "Microsoft.AspNetCore.Connections.Abstractions": "9.0.3" + "Microsoft.AspNetCore.Connections.Abstractions": "10.0.1" } }, "Microsoft.AspNetCore.SignalR.Client.Core": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "R2N03AK5FH8KIENfJGER4SgjJFMJTBiYuLbovbRunp5R4knO+iysfbYMfEFO3kn98ElWr/747dS4AeWQOEEQsg==", + "resolved": "10.0.1", + "contentHash": "lkPaGkCtVibYBzUzO8gTGsX39L5XeZl8KArueePWMYqs6c2G58ch4fmKL0qMRqsFQ84uqd+5uOJ96dClusC+IQ==", "dependencies": { - "Microsoft.AspNetCore.SignalR.Common": "9.0.3", - "Microsoft.AspNetCore.SignalR.Protocols.Json": "9.0.3", - "Microsoft.Extensions.DependencyInjection": "9.0.3", - "Microsoft.Extensions.Logging": "9.0.3", - "System.Threading.Channels": "9.0.3" + "Microsoft.AspNetCore.SignalR.Common": "10.0.1", + "Microsoft.AspNetCore.SignalR.Protocols.Json": "10.0.1", + "Microsoft.Extensions.DependencyInjection": "10.0.1", + "Microsoft.Extensions.Logging": "10.0.1" } }, "Microsoft.AspNetCore.SignalR.Common": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "/568tq8YVas1mDgeScmQdQV4ZDRjdyqDS3rAo17R5Bs4puMaNM80wQSwcvsmN5gSwH6L/XRTmD1J1uRIyKXrCg==", + "resolved": "10.0.1", + "contentHash": "BwXSW2/fksJMY17fTInCb434TznOovgkON8IP6BFI54K0kiZkRDvcFxnUx27DTFAgDphz3oed8RvzFo/yGTkbQ==", "dependencies": { - "Microsoft.AspNetCore.Connections.Abstractions": "9.0.3", - "Microsoft.Extensions.Options": "9.0.3" + "Microsoft.AspNetCore.Connections.Abstractions": "10.0.1", + "Microsoft.Extensions.Options": "10.0.1" } }, "Microsoft.AspNetCore.SignalR.Protocols.Json": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "jvOdsquqrbWMP3/Aq4s8/yVeCxBkjvxarv/2WgubKkQT8nZ46aKY3Rvj1uolp4N3TuaMGlnd6mhK/tF7jCat2Q==", + "resolved": "10.0.1", + "contentHash": "mv5FggqTA1uIOhgrp+ZixYICplvCMNvpDBvV5DfT9nLJ9luteaskbqsNoaPRLZi23ZKpg20Y9ZLhxkk2C91gMA==", "dependencies": { - "Microsoft.AspNetCore.SignalR.Common": "9.0.3" + "Microsoft.AspNetCore.SignalR.Common": "10.0.1" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "RIEeZxWYm77+OWLwgik7DzSVSONjqkmcbuCb1koZdGAV7BgOUWnLz80VMyHZMw3onrVwFCCMHBBdruBPuQTvkg==", + "resolved": "10.0.1", + "contentHash": "njoRekyMIK+smav8B6KL2YgIfUtlsRNuT7wvurpLW+m/hoRKVnoELk2YxnUnWRGScCd1rukLMxShwLqEOKowDg==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", - "Microsoft.Extensions.Primitives": "9.0.3" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", + "Microsoft.Extensions.Primitives": "10.0.1" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "q5qlbm6GRUrle2ZZxy9aqS/wWoc+mRD3JeP6rcpiJTh5XcemYkplAcJKq8lU11ZfPom5lfbZZfnQvDqcUhqD5Q==", + "resolved": "10.0.1", + "contentHash": "kPlU11hql+L9RjrN2N9/0GcRcRcZrNFlLLjadasFWeBORT6pL6OE+RYRk90GGCyVGSxTK+e1/f3dsMj5zpFFiQ==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.3" + "Microsoft.Extensions.Primitives": "10.0.1" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "ad82pYBUSQbd3WIboxsS1HzFdRuHKRa2CpYwie/o6dZAxUjt62yFwjoVdM7Iw2VO5fHV1rJwa7jJZBNZin0E7Q==", + "resolved": "10.0.1", + "contentHash": "Lp4CZIuTVXtlvkAnTq6QvMSW7+H62gX2cU2vdFxHQUxvrWTpi7LwYI3X+YAyIS0r12/p7gaosco7efIxL4yFNw==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.3" + "Microsoft.Extensions.Configuration": "10.0.1", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.1" } }, "Microsoft.Extensions.Configuration.CommandLine": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "rVwz4ml/Jve/QzzUlyTVOKXVZ37op9RK6Ize4uPmJ3S5c2ErExoy816+dslBQ06ZrFq8M9bpnV5LVBuPD1ONHQ==", + "resolved": "10.0.1", + "contentHash": "s5cxcdtIig66YT3J+7iHflMuorznK8kXuwBBPHMp4KImx5ZGE3FRa1Nj9fI/xMwFV+KzUMjqZ2MhOedPH8LiBQ==", "dependencies": { - "Microsoft.Extensions.Configuration": "9.0.3", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.3" + "Microsoft.Extensions.Configuration": "10.0.1", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.1" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "fo84UIa8aSBG3pOtzLsgkj1YkOVfYFy2YWcRTCevHHAkuVsxnYnKBrcW2pyFgqqfQ/rT8K1nmRXHDdQIZ8PDig==", + "resolved": "10.0.1", + "contentHash": "csD8Eps3HQ3yc0x6NhgTV+aIFKSs3qVlVCtFnMHz/JOjnv7eEj/qSXKXiK9LzBzB1qSfAVqFnB5iaX2nUmagIQ==", "dependencies": { - "Microsoft.Extensions.Configuration": "9.0.3", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.3" + "Microsoft.Extensions.Configuration": "10.0.1", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.1" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "tBNMSDJ2q7WQK2zwPhHY5I/q95t7sf6dT079mGrNm0yOZF/gM9JvR/LtCb/rwhRmh7A6XMnzv5WbpCh9KLq9EQ==", + "resolved": "10.0.1", + "contentHash": "N/6GiwiZFCBFZDk3vg1PhHW3zMqqu5WWpmeZAA9VTXv7Q8pr8NZR/EQsH0DjzqydDksJtY6EQBsu81d5okQOlA==", "dependencies": { - "Microsoft.Extensions.Configuration": "9.0.3", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.3", - "Microsoft.Extensions.FileProviders.Physical": "9.0.3", - "Microsoft.Extensions.Primitives": "9.0.3" + "Microsoft.Extensions.Configuration": "10.0.1", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.1", + "Microsoft.Extensions.FileProviders.Physical": "10.0.1", + "Microsoft.Extensions.Primitives": "10.0.1" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "mjkp3ZwynNacZk4uq93I0DyCY48FZmi3yRV0xlfeDuWh44KcDunPXHwt8IWr4kL7cVM6eiFVe6YTJg97KzUAUA==", + "resolved": "10.0.1", + "contentHash": "0zW3eYBJlRctmgqk5s0kFIi5o5y2g80mvGCD8bkYxREPQlKUnr0ndU/Sop+UDIhyWN0fIi4RW63vo7BKTi7ncA==", "dependencies": { - "Microsoft.Extensions.Configuration": "9.0.3", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", - "Microsoft.Extensions.Configuration.FileExtensions": "9.0.3", - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.3" + "Microsoft.Extensions.Configuration": "10.0.1", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.1", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.1" } }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "vwkBQ5jqmfX7nD7CFvB3k1uSeNBKRcYRDvlk3pxJzJfm/cgT4R+hQg5AFXW/1aLKjz0q7brpRocHC5GK2sjvEw==", + "resolved": "10.0.1", + "contentHash": "ULEJ0nkaW90JYJGkFujPcJtADXcJpXiSOLbokPcWJZ8iDbtDINifEYAUVqZVr81IDNTrRFul6O8RolOKOsgFPg==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", - "Microsoft.Extensions.Configuration.Json": "9.0.3", - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.3", - "Microsoft.Extensions.FileProviders.Physical": "9.0.3" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", + "Microsoft.Extensions.Configuration.Json": "10.0.1", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.1", + "Microsoft.Extensions.FileProviders.Physical": "10.0.1" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "lDbxJpkl6X8KZGpkAxgrrthQ42YeiR0xjPp7KPx+sCPc3ZbpaIbjzd0QQ+9kDdK2RU2DOl3pc6tQyAgEZY3V0A==", + "resolved": "10.0.1", + "contentHash": "zerXV0GAR9LCSXoSIApbWn+Dq1/T+6vbXMHGduq1LoVQRHT0BXsGQEau0jeLUBUcsoF/NaUT8ADPu8b+eNcIyg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "TfaHPSe39NyL2wxkisRxXK7xvHGZYBZ+dy3r+mqGvnxKgAPdHkMu3QMQZI4pquP6W5FIQBqs8FJpWV8ffCgDqQ==" + "resolved": "10.0.1", + "contentHash": "oIy8fQxxbUsSrrOvgBqlVgOeCtDmrcynnTG+FQufcUWBrwyPfwlUkCDB2vaiBeYPyT+20u9/HeuHeBf+H4F/8g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "gqhbIq6adm0+/9IlDYmchekoxNkmUTm7rfTG3k4zzoQkjRuD8TQGwL1WnIcTDt4aQ+j+Vu0OQrjI8GlpJQQhIA==", + "resolved": "10.0.1", + "contentHash": "YaocqxscJLxLit0F5yq2XyB+9C7rSRfeTL7MJIl7XwaOoUO3i0EqfO2kmtjiRduYWw7yjcSINEApYZbzjau2gQ==", "dependencies": { - "Microsoft.Extensions.Configuration": "9.0.3", - "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.3", - "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.3" + "Microsoft.Extensions.Configuration": "10.0.1", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.1", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.1" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "/fn0Xe8t+3YbMfwyTk4hFirWyAG1pBA5ogVYsrKAuuD2gbqOWhFuSA28auCmS3z8Y2eq3miDIKq4pFVRWA+J6g==", + "resolved": "10.0.1", + "contentHash": "QMoMrkNpnQym5mpfdxfxpRDuqLpsOuztguFvzH9p+Ex+do+uLFoi7UkAsBO4e9/tNR3eMFraFf2fOAi2cp3jjA==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", - "Microsoft.Extensions.Options": "9.0.3" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", + "Microsoft.Extensions.Options": "10.0.1" } }, "Microsoft.Extensions.Features": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "jZuO3APLh0ePwtT9PDxiMdPwpDdct/kuExlXLCZZ+XFje/Xt815MM827EFJuxTBAbL148ywyfJyjIZ92osP5WA==" + "resolved": "10.0.1", + "contentHash": "kxUFH96eZsr63CTKGDaUUaXks7JxUxt4xs91lXeqBQmtyIEjDll2detJlBDuZTZIdmJOFoSH+YmnGr/mImcvXA==" }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "umczZ3+QPpzlrW/lkvy+IB0p52+qZ5w++aqx2lTCMOaPKzwcbVdrJgiQ3ajw5QWBp7gChLUiCYkSlWUpfjv24g==", + "resolved": "10.0.1", + "contentHash": "+b3DligYSZuoWltU5YdbMpIEUHNZPgPrzWfNiIuDkMdqOl93UxYB5KzS3lgpRfTXJhTNpo/CZ8w/sTkDTPDdxQ==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.3" + "Microsoft.Extensions.Primitives": "10.0.1" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "th2+tQBV5oWjgKhip9GjiIv2AEK3QvfAO3tZcqV3F3dEt5D6Gb411RntCj1+8GS9HaRRSxjSGx/fCrMqIjkb1Q==", + "resolved": "10.0.1", + "contentHash": "4bxzGXIzZnz0Bf7czQ72jGvpOqJsRW/44PS7YLFXTTnu6cNcPvmSREDvBoH0ZVP2hAbMfL4sUoCUn54k70jPWw==", "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.3", - "Microsoft.Extensions.FileSystemGlobbing": "9.0.3", - "Microsoft.Extensions.Primitives": "9.0.3" + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.1", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.1", + "Microsoft.Extensions.Primitives": "10.0.1" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "Rec77KHk4iNpFznHi5/6wF3MlUDcKqg26t8gRYbUm1PSukZ4B6mrXpZsJSNOiwyhhQVkjYbaoZxi5XJgRQ5lFg==" + "resolved": "10.0.1", + "contentHash": "49dFvGJjLSwGn76eHnP1gBvCJkL8HRYpCrG0DCvsP6wRpEQRLN2Fq8rTxbP+6jS7jmYKCnSVO5C65v4mT3rzeA==" }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "rHabYVhQsGYNfgnfnYLqZRx/hLe85i6jW5rnDjA9pjt3x7yjPv8T/EXcgN5T9T38FAVwZRA+RMGUkEHbxvCOBQ==", + "resolved": "10.0.1", + "contentHash": "qmoQkVZcbm4/gFpted3W3Y+1kTATZTcUhV3mRkbtpfBXlxWCHwh/2oMffVcCruaGOfJuEnyAsGyaSUouSdECOw==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", - "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.3", - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.3", - "Microsoft.Extensions.Logging.Abstractions": "9.0.3" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.1", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.1", + "Microsoft.Extensions.Logging.Abstractions": "10.0.1" } }, "Microsoft.Extensions.Logging": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "utIi2R1nm+PCWkvWBf1Ou6LWqg9iLfHU23r8yyU9VCvda4dEs7xbTZSwGa5KuwbpzpgCbHCIuKaFHB3zyFmnGw==", + "resolved": "10.0.1", + "contentHash": "9ItMpMLFZFJFqCuHLLbR3LiA4ahA8dMtYuXpXl2YamSDWZhYS9BruPprkftY0tYi2bQ0slNrixdFm+4kpz1g5w==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "9.0.3", - "Microsoft.Extensions.Logging.Abstractions": "9.0.3", - "Microsoft.Extensions.Options": "9.0.3" + "Microsoft.Extensions.DependencyInjection": "10.0.1", + "Microsoft.Extensions.Logging.Abstractions": "10.0.1", + "Microsoft.Extensions.Options": "10.0.1" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "H/MBMLt9A/69Ux4OrV7oCKt3DcMT04o5SCqDolulzQA66TLFEpYYb4qedMs/uwrLtyHXGuDGWKZse/oa8W9AZw==", + "resolved": "10.0.1", + "contentHash": "YkmyiPIWAXVb+lPIrM0LE5bbtLOJkCiRTFiHpkVOvhI7uTvCfoOHLEN0LcsY56GpSD7NqX3gJNpsaDe87/B3zg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "eVZsaKNyK0g0C1qp0mmn4Q2PiX+bXdkz8+zVkXyVMk8IvoWfmTjLjEq1MQlwt1A22lToANPiUrxPJ7Tt3V5puw==", + "resolved": "10.0.1", + "contentHash": "Zg8LLnfZs5o2RCHD/+9NfDtJ40swauemwCa7sI8gQoAye/UJHRZNpCtC7a5XE7l9Z7mdI8iMWnLZ6m7Q6S3jLg==", "dependencies": { - "Microsoft.Extensions.Configuration": "9.0.3", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", - "Microsoft.Extensions.Configuration.Binder": "9.0.3", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", - "Microsoft.Extensions.Logging": "9.0.3", - "Microsoft.Extensions.Logging.Abstractions": "9.0.3", - "Microsoft.Extensions.Options": "9.0.3", - "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.3" + "Microsoft.Extensions.Configuration": "10.0.1", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", + "Microsoft.Extensions.Configuration.Binder": "10.0.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", + "Microsoft.Extensions.Logging": "10.0.1", + "Microsoft.Extensions.Logging.Abstractions": "10.0.1", + "Microsoft.Extensions.Options": "10.0.1", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.1" } }, "Microsoft.Extensions.Logging.Console": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "o9VXLOdpTAro1q7ZThIB3S8OHrRn5pr8cFUCiN85fiwlfAt2DhU4ZIfHy+jCNbf7y7S5Exbr3dlDE8mKNrs0Yg==", + "resolved": "10.0.1", + "contentHash": "38Q8sEHwQ/+wVO/mwQBa0fcdHbezFpusHE+vBw/dSr6Fq/kzZm3H/NQX511Jki/R3FHd64IY559gdlHZQtYeEA==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", - "Microsoft.Extensions.Logging": "9.0.3", - "Microsoft.Extensions.Logging.Abstractions": "9.0.3", - "Microsoft.Extensions.Logging.Configuration": "9.0.3", - "Microsoft.Extensions.Options": "9.0.3" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", + "Microsoft.Extensions.Logging": "10.0.1", + "Microsoft.Extensions.Logging.Abstractions": "10.0.1", + "Microsoft.Extensions.Logging.Configuration": "10.0.1", + "Microsoft.Extensions.Options": "10.0.1" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "BlKgvNYjD6mY5GXpMCf9zPAsrovMgW5mzCOT7SpoOSyI1478zldf+7PKvDIscC277z5zjSO3yi/OuIWpnTZmdA==", + "resolved": "10.0.1", + "contentHash": "VqfTvbX9C6BA0VeIlpzPlljnNsXxiI5CdUHb9ksWERH94WQ6ft3oLGUAa4xKcDGu4xF+rIZ8wj7IOAd6/q7vGw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", - "Microsoft.Extensions.Logging": "9.0.3", - "Microsoft.Extensions.Logging.Abstractions": "9.0.3" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", + "Microsoft.Extensions.Logging": "10.0.1", + "Microsoft.Extensions.Logging.Abstractions": "10.0.1" } }, "Microsoft.Extensions.Logging.EventLog": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "/+elZUHGgB3oHKO9St/Ql/qfze9O+UbXj+9FOj1gIshLCFXcPlhpKoI11jE6eIV0kbs1P/EeffJl4KDFyvAiJQ==", + "resolved": "10.0.1", + "contentHash": "Zp9MM+jFCa7oktIug62V9eNygpkf+6kFVatF+UC/ODeUwIr5givYKy8fYSSI9sWdxqDqv63y1x0mm2VjOl8GOw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", - "Microsoft.Extensions.Logging": "9.0.3", - "Microsoft.Extensions.Logging.Abstractions": "9.0.3", - "Microsoft.Extensions.Options": "9.0.3", - "System.Diagnostics.EventLog": "9.0.3" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", + "Microsoft.Extensions.Logging": "10.0.1", + "Microsoft.Extensions.Logging.Abstractions": "10.0.1", + "Microsoft.Extensions.Options": "10.0.1", + "System.Diagnostics.EventLog": "10.0.1" } }, "Microsoft.Extensions.Logging.EventSource": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "hgG0EGEHnngQFQNqJ5ungEykqaQ5Tik0Gpkb38pea2a5cR3pWlZR4vuYLDdtTgSiKEKByXz/3wNQ7qAqXamEEA==", + "resolved": "10.0.1", + "contentHash": "WnFvZP+Y+lfeNFKPK/+mBpaCC7EeBDlobrQOqnP7rrw/+vE7yu8Rjczum1xbC0F/8cAHafog84DMp9200akMNQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", - "Microsoft.Extensions.Logging": "9.0.3", - "Microsoft.Extensions.Logging.Abstractions": "9.0.3", - "Microsoft.Extensions.Options": "9.0.3", - "Microsoft.Extensions.Primitives": "9.0.3" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", + "Microsoft.Extensions.Logging": "10.0.1", + "Microsoft.Extensions.Logging.Abstractions": "10.0.1", + "Microsoft.Extensions.Options": "10.0.1", + "Microsoft.Extensions.Primitives": "10.0.1" } }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "xE7MpY70lkw1oiid5y6FbL9dVw8oLfkx8RhSNGN8sSzBlCqGn0SyT3Fqc8tZnDaPIq7Z8R9RTKlS564DS+MV3g==", + "resolved": "10.0.1", + "contentHash": "G6VVwywpJI4XIobetGHwg7wDOYC2L2XBYdtskxLaKF/Ynb5QBwLl7Q//wxAR2aVCLkMpoQrjSP9VoORkyddsNQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", - "Microsoft.Extensions.Primitives": "9.0.3" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", + "Microsoft.Extensions.Primitives": "10.0.1" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "PcyYHQglKnWVZHSPaL6v2qnfsIuFw8tSq7cyXHg3OeuDVn/CqmdWUjRiZomCF/Gi+qCi+ksz0lFphg2cNvB8zQ==", + "resolved": "10.0.1", + "contentHash": "pL78/Im7O3WmxHzlKUsWTYchKL881udU7E26gCD3T0+/tPhWVfjPwMzfN/MRKU7aoFYcOiqcG2k1QTlH5woWow==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", - "Microsoft.Extensions.Configuration.Binder": "9.0.3", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", - "Microsoft.Extensions.Options": "9.0.3", - "Microsoft.Extensions.Primitives": "9.0.3" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.1", + "Microsoft.Extensions.Configuration.Binder": "10.0.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1", + "Microsoft.Extensions.Options": "10.0.1", + "Microsoft.Extensions.Primitives": "10.0.1" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "yCCJHvBcRyqapMSNzP+kTc57Eaavq2cr5Tmuil6/XVnipQf5xmskxakSQ1enU6S4+fNg3sJ27WcInV64q24JsA==" + "resolved": "10.0.1", + "contentHash": "DO8XrJkp5x4PddDuc/CH37yDBCs9BYN6ijlKyR3vMb55BP1Vwh90vOX8bNfnKxr5B2qEI3D8bvbY1fFbDveDHQ==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "8.7.0", - "contentHash": "OQd5aVepYvh5evOmBMeAYjMIpEcTf1ZCBZaU7Nh/RlhhdXefjFDJeP1L2F2zeNT1unFr+wUu/h3Ac2Xb4BXU6w==" + "resolved": "8.15.0", + "contentHash": "e/DApa1GfxUqHSBHcpiQg8yaghKAvFVBQFcWh25jNoRobDZbduTUACY8bZ54eeGWXvimGmEDdF0zkS5Dq16XPQ==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "8.7.0", - "contentHash": "uzsSAWhNhbrkWbQKBTE8QhzviU6sr3bJ1Bkv7gERlhswfSKOp7HsxTRLTPBpx/whQ/GRRHEwMg8leRIPbMrOgw==", + "resolved": "8.15.0", + "contentHash": "3513f5VzvOZy3ELd42wGnh1Q3e83tlGAuXFSNbENpgWYoAhLLzgFtd5PiaOPGAU0gqKhYGVzKavghLUGfX3HQg==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.7.0" + "Microsoft.IdentityModel.Tokens": "8.15.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "8.7.0", - "contentHash": "Bs0TznPAu+nxa9rAVHJ+j3CYECHJkT3tG8AyBfhFYlT5ldsDhoxFT7J+PKxJHLf+ayqWfvDZHHc4639W2FQCxA==", + "resolved": "8.15.0", + "contentHash": "1gJLjhy0LV2RQMJ9NGzi5Tnb2l+c37o8D8Lrk2mrvmb6OQHZ7XJstd/XxvncXgBpad4x9CGXdipbZzJJCXKyAg==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.7.0" + "Microsoft.IdentityModel.Abstractions": "8.15.0" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "8.7.0", - "contentHash": "5Z6voXjRXAnGklhmZd1mKz89UhcF5ZQQZaZc2iKrOuL4Li1UihG2vlJx8IbiFAOIxy/xdbsAm0A+WZEaH5fxng==", + "resolved": "8.15.0", + "contentHash": "zUE9ysJXBtXlHHRtcRK3Sp8NzdCI1z/BRDTXJQ2TvBoI0ENRtnufYIep0O5TSCJRJGDwwuLTUx+l/bEYZUxpCA==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2", - "Microsoft.IdentityModel.Logging": "8.7.0" + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.IdentityModel.Logging": "8.15.0" } }, "Microsoft.NET.StringTools": { @@ -503,20 +523,78 @@ "resolved": "17.6.3", "contentHash": "N0ZIanl1QCgvUumEL1laasU0a7sOE5ZwLZVTn0pAePnfhq8P7SvTjF8Axq+CnavuQkmdQpGNXQ1efZtu5kDFbA==" }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "SharpDX": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "3pv0LFMvfK/dv1qISJnn8xBeeT6R/FRvr0EV4KI2DGsL84Qlv6P7isWqxGyU0LCwlSVCJN3jgHJ4Bl0KI2PJww==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "SharpDX.D3DCompiler": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "Rnsd6Ilp127xbXqhTit8WKFQUrXwWxqVGpglyWDNkIBCk0tWXNQEjrJpsl0KAObzyZaa33+EXAikLVt5fnd3GA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0" + } + }, + "SharpDX.Direct2D1": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "Qs8LzDMaQf1u3KB8ArHu9pDv6itZ++QXs99a/bVAG+nKr0Hx5NG4mcN5vsfE0mVR2TkeHfeUm4PksRah6VUPtA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0", + "SharpDX.DXGI": "4.2.0" + } + }, + "SharpDX.Direct3D11": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "oTm/iT5X/IIuJ8kNYP+DTC/MhBhqtRF5dbgPPFgLBdQv0BKzNTzXQQXd7SveBFjQg6hXEAJ2jGCAzNYvGFc9LA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0", + "SharpDX.DXGI": "4.2.0" + } + }, + "SharpDX.DXGI": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "UjKqkgWc8U+SP+j3LBzFP6OB6Ntapjih7Xo+g1rLcsGbIb5KwewBrBChaUu7sil8rWoeVU/k0EJd3SMN4VqNZw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0" + } + }, + "SharpDX.Mathematics": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "R2pcKLgdsP9p5WyTjHmGOZ0ka0zASAZYc6P4L6rSvjYhf6klGYbent7MiVwbkwkt9dD44p5brjy5IwAnVONWGw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0" + } + }, "System.Diagnostics.EventLog": { "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "0nDJBZ06DVdTG2vvCZ4XjazLVaFawdT0pnji23ISX8I8fEOlRJyzH2I0kWiAbCtFwry2Zir4qE4l/GStLATfFw==" - }, - "System.Net.ServerSentEvents": { - "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "Vs/C2V27bjtwLqYag9ATzHilcUn8VQTICre4jSBMGFUeSTxEZffTjb+xZwjcmPsVAjmSZmBI5N7Ezq8UFvqQQg==" - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "Ao0iegVONKYVw0eWxJv0ArtMVfkFjgyyYKtUXru6xX5H95flSZWW3QCavD4PAgwpc0ETP38kGHaYbPzSE7sw2w==" + "resolved": "10.0.1", + "contentHash": "xfaHEHVDkMOOZR5S6ZGezD0+vekdH1Nx/9Ih8/rOqOGSOk1fxiN3u94bYkBW/wigj0Uw2Wt3vvRj9mtYdgwEjw==" }, "lightlesssync.api": { "type": "Project", @@ -524,8 +602,37 @@ "MessagePack.Annotations": "[3.1.3, )" } }, + "ottergui": { + "type": "Project", + "dependencies": { + "JetBrains.Annotations": "[2024.3.0, )", + "Microsoft.Extensions.DependencyInjection": "[9.0.2, )" + } + }, "penumbra.api": { "type": "Project" + }, + "penumbra.gamedata": { + "type": "Project", + "dependencies": { + "FlatSharp.Compiler": "[7.9.0, )", + "FlatSharp.Runtime": "[7.9.0, )", + "OtterGui": "[1.0.0, )", + "Penumbra.Api": "[5.13.0, )", + "Penumbra.String": "[1.0.7, )" + } + }, + "penumbra.string": { + "type": "Project" + }, + "pictomancy": { + "type": "Project", + "dependencies": { + "SharpDX.D3DCompiler": "[4.2.0, )", + "SharpDX.Direct2D1": "[4.2.0, )", + "SharpDX.Direct3D11": "[4.2.0, )", + "SharpDX.Mathematics": "[4.2.0, )" + } } } } diff --git a/OtterGui b/OtterGui new file mode 160000 index 0000000..ff1e654 --- /dev/null +++ b/OtterGui @@ -0,0 +1 @@ +Subproject commit ff1e6543845e3b8c53a5f8b240bc38faffb1b3bf diff --git a/Penumbra.Api b/Penumbra.Api new file mode 160000 index 0000000..52a3216 --- /dev/null +++ b/Penumbra.Api @@ -0,0 +1 @@ +Subproject commit 52a3216a525592205198303df2844435e382cf87 diff --git a/Penumbra.GameData b/Penumbra.GameData new file mode 160000 index 0000000..0e973ed --- /dev/null +++ b/Penumbra.GameData @@ -0,0 +1 @@ +Subproject commit 0e973ed6eace6afd31cd298f8c58f76fa8d5ef60 diff --git a/Penumbra.String b/Penumbra.String new file mode 160000 index 0000000..9bd016f --- /dev/null +++ b/Penumbra.String @@ -0,0 +1 @@ +Subproject commit 9bd016fbef5fb2de467dd42165267fdd93cd9592 diff --git a/PenumbraAPI b/PenumbraAPI deleted file mode 160000 index a2f8923..0000000 --- a/PenumbraAPI +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a2f89235464ea6cc25bb933325e8724b73312aa6 diff --git a/ffxiv_pictomancy b/ffxiv_pictomancy new file mode 160000 index 0000000..66c9667 --- /dev/null +++ b/ffxiv_pictomancy @@ -0,0 +1 @@ +Subproject commit 66c96678a29454f178c681d8920a8ee0a9d50c40