From 3034660770059bc5f09a9a955aa611f3c142e6e1 Mon Sep 17 00:00:00 2001 From: Professor Fartsalot Date: Sat, 30 Aug 2025 23:16:59 -0400 Subject: [PATCH] Restore latest from ProfessorFartsalot/SnowcloakClient --- Glamourer.Api/.editorconfig | 3625 +++++++++++++++++ Glamourer.Api/.gitignore | 3 + Glamourer.Api/Api/IGlamourerApi.cs | 14 + Glamourer.Api/Api/IGlamourerApiBase.cs | 11 + Glamourer.Api/Api/IGlamourerApiDesigns.cs | 33 + Glamourer.Api/Api/IGlamourerApiItems.cs | 80 + Glamourer.Api/Api/IGlamourerApiState.cs | 124 + Glamourer.Api/Enums/ApiBonusSlot.cs | 11 + Glamourer.Api/Enums/ApiEquipSlot.cs | 45 + Glamourer.Api/Enums/ApplyFlag.cs | 31 + Glamourer.Api/Enums/GlamourerApiEc.cs | 29 + Glamourer.Api/Enums/SetMetaFlag.cs | 11 + Glamourer.Api/Enums/StateChangeType.cs | 47 + Glamourer.Api/Enums/StateFinalizationType.cs | 36 + Glamourer.Api/Glamourer.Api.csproj | 34 + .../Glamourer.Api.csproj.DotSettings | 2 + Glamourer.Api/GlobalUsings.cs | 4 + Glamourer.Api/Helpers/ActionSubscriber.cs | 114 + Glamourer.Api/Helpers/EventProvider.cs | 234 ++ Glamourer.Api/Helpers/EventSubscriber.cs | 394 ++ Glamourer.Api/Helpers/FuncProvider.cs | 224 + Glamourer.Api/Helpers/FuncSubscriber.cs | 217 + Glamourer.Api/Helpers/PluginLogHelper.cs | 26 + Glamourer.Api/IpcSubscribers/Designs.cs | 52 + Glamourer.Api/IpcSubscribers/Items.cs | 110 + .../IpcSubscribers/Legacy/Designs.cs | 52 + Glamourer.Api/IpcSubscribers/Legacy/Items.cs | 66 + .../IpcSubscribers/Legacy/PluginState.cs | 15 + Glamourer.Api/IpcSubscribers/Legacy/State.cs | 250 ++ Glamourer.Api/IpcSubscribers/PluginState.cs | 51 + Glamourer.Api/IpcSubscribers/State.cs | 311 ++ Glamourer.Api/README.md | 4 + Glamourer.Api/packages.lock.json | 13 + LICENSE | 617 +++ LICENSE_MIT | 21 + MareAPI/.gitignore | 350 ++ MareAPI/LICENSE | 21 + .../MareSynchronosAPI/Data/CharacterData.cs | 36 + MareAPI/MareSynchronosAPI/Data/ChatMessage.cs | 11 + .../Data/Comparer/GroupDataComparer.cs | 19 + .../Data/Comparer/GroupDtoComparer.cs | 23 + .../Data/Comparer/GroupPairDtoComparer.cs | 20 + .../Data/Comparer/UserDataComparer.cs | 20 + .../Data/Comparer/UserDtoComparer.cs | 20 + .../Data/Enum/GroupPermissions.cs | 11 + .../Data/Enum/GroupUserInfo.cs | 9 + .../Data/Enum/GroupUserPermissions.cs | 11 + .../Data/Enum/MessageSeverity.cs | 8 + .../MareSynchronosAPI/Data/Enum/ObjectKind.cs | 9 + .../Data/Enum/UserPermissions.cs | 12 + .../Extensions/GroupPermissionsExtensions.cs | 50 + .../Extensions/GroupUserInfoExtensions.cs | 28 + .../GroupUserPermissionsExtensions.cs | 50 + .../Extensions/UserPermissionsExtensions.cs | 61 + .../Data/FileReplacementData.cs | 30 + MareAPI/MareSynchronosAPI/Data/GroupData.cs | 10 + .../Data/SignedChatMessage.cs | 14 + MareAPI/MareSynchronosAPI/Data/UserData.cs | 10 + .../Dto/Account/RegisterReplyDto.cs | 12 + .../Dto/Account/RegisterReplyV2Dto.cs | 11 + MareAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs | 11 + .../Dto/CharaData/AccessTypeDto.cs | 9 + .../Dto/CharaData/CharaDataDownloadDto.cs | 14 + .../Dto/CharaData/CharaDataDto.cs | 9 + .../Dto/CharaData/CharaDataFullDto.cs | 88 + .../Dto/CharaData/CharaDataMetaInfoDto.cs | 11 + .../Dto/CharaData/CharaDataUpdateDto.cs | 20 + .../Dto/CharaData/ShareTypeDto.cs | 7 + .../Dto/Chat/GroupChatMsgDto.cs | 13 + .../Dto/Chat/UserChatMsgDto.cs | 11 + .../MareSynchronosAPI/Dto/ConnectionDto.cs | 25 + .../Dto/Files/DownloadFileDto.cs | 14 + .../Dto/Files/FilesSendDto.cs | 13 + .../Dto/Files/ITransferFileDto.cs | 8 + .../Dto/Files/UploadFileDto.cs | 11 + .../Dto/Group/BannedGroupUserDto.cs | 19 + .../MareSynchronosAPI/Dto/Group/GroupDto.cs | 13 + .../Dto/Group/GroupFullInfoDto.cs | 12 + .../Dto/Group/GroupInfoDto.cs | 16 + .../Dto/Group/GroupPairDto.cs | 12 + .../Dto/Group/GroupPairFullInfoDto.cs | 12 + .../Dto/Group/GroupPairUserInfoDto.cs | 8 + .../Dto/Group/GroupPairUserPermissionDto.cs | 8 + .../Dto/Group/GroupPasswordDto.cs | 7 + .../Dto/Group/GroupPermissionDto.cs | 8 + .../MareSynchronosAPI/Dto/SystemInfoDto.cs | 9 + .../Dto/User/OnlineUserCharaDataDto.cs | 7 + .../Dto/User/OnlineUserIdentDto.cs | 7 + .../Dto/User/UserCharaDataMessageDto.cs | 7 + MareAPI/MareSynchronosAPI/Dto/User/UserDto.cs | 7 + .../MareSynchronosAPI/Dto/User/UserPairDto.cs | 12 + .../Dto/User/UserPermissionsDto.cs | 8 + .../Dto/User/UserProfileDto.cs | 7 + .../Dto/User/UserProfileReportDto.cs | 7 + .../MareSynchronos.API.csproj | 13 + .../MareSynchronosAPI/MareSynchronosAPI.sln | 25 + MareAPI/MareSynchronosAPI/Routes/MareAuth.cs | 14 + MareAPI/MareSynchronosAPI/Routes/MareFiles.cs | 45 + MareAPI/MareSynchronosAPI/SignalR/IMareHub.cs | 144 + .../SignalR/IMareHubClient.cs | 62 + MareSynchronos.sln | 46 + MareSynchronos/.editorconfig | 120 + MareSynchronos/FileCache/CacheMonitor.cs | 859 ++++ MareSynchronos/FileCache/FileCacheEntity.cs | 30 + MareSynchronos/FileCache/FileCacheManager.cs | 556 +++ MareSynchronos/FileCache/FileCompactor.cs | 250 ++ MareSynchronos/FileCache/FileState.cs | 8 + .../FileCache/TransientResourceManager.cs | 313 ++ MareSynchronos/GlobalSuppressions.cs | 8 + .../Interop/BlockedCharacterHandler.cs | 42 + MareSynchronos/Interop/DalamudLogger.cs | 55 + .../Interop/DalamudLoggingProvider.cs | 44 + .../DalamudLoggingProviderExtensions.cs | 20 + MareSynchronos/Interop/GameChatHooks.cs | 333 ++ MareSynchronos/Interop/GameModel/MdlFile.cs | 259 ++ MareSynchronos/Interop/Ipc/IIpcCaller.cs | 7 + MareSynchronos/Interop/Ipc/IpcCallerBrio.cs | 147 + .../Interop/Ipc/IpcCallerCustomize.cs | 139 + .../Interop/Ipc/IpcCallerGlamourer.cs | 253 ++ MareSynchronos/Interop/Ipc/IpcCallerHeels.cs | 93 + .../Interop/Ipc/IpcCallerHonorific.cs | 135 + MareSynchronos/Interop/Ipc/IpcCallerMare.cs | 44 + .../Interop/Ipc/IpcCallerMoodles.cs | 104 + .../Interop/Ipc/IpcCallerPenumbra.cs | 366 ++ .../Interop/Ipc/IpcCallerPetNames.cs | 158 + MareSynchronos/Interop/Ipc/IpcManager.cs | 68 + MareSynchronos/Interop/Ipc/IpcProvider.cs | 196 + MareSynchronos/Interop/Ipc/RedrawManager.cs | 54 + MareSynchronos/Interop/VfxSpawnManager.cs | 203 + .../CharaDataConfigService.cs | 11 + .../ConfigurationExtensions.cs | 13 + .../ConfigurationMigrator.cs | 25 + .../ConfigurationSaveService.cs | 137 + .../ConfigurationServiceBase.cs | 141 + .../Configurations/CharaDataConfig.cs | 19 + .../Configurations/IMareConfiguration.cs | 6 + .../Configurations/MareConfig.cs | 78 + .../Configurations/PlayerPerformanceConfig.cs | 16 + .../Configurations/RemoteConfigCache.cs | 13 + .../Configurations/ServerBlockConfig.cs | 10 + .../Configurations/ServerConfig.cs | 17 + .../Configurations/ServerTagConfig.cs | 10 + .../Configurations/SyncshellConfig.cs | 10 + .../Configurations/TransientConfig.cs | 7 + .../Configurations/UidNotesConfig.cs | 10 + .../Configurations/XivDataStorageConfig.cs | 11 + .../MareConfiguration/IConfigService.cs | 12 + .../MareConfiguration/MareConfigService.cs | 14 + .../Models/Authentication.cs | 9 + .../Models/CharaDataFavorite.cs | 8 + .../Models/DownloadSpeeds.cs | 8 + .../Models/NotificationLocation.cs | 16 + .../Models/Obsolete/ServerStorageV0.cs | 29 + .../MareConfiguration/Models/SecretKey.cs | 8 + .../Models/ServerBlockStorage.cs | 8 + .../Models/ServerNotesStorage.cs | 9 + .../Models/ServerShellStorage.cs | 7 + .../MareConfiguration/Models/ServerStorage.cs | 11 + .../Models/ServerTagStorage.cs | 9 + .../MareConfiguration/Models/ShellConfig.cs | 10 + .../Models/TextureShrinkMode.cs | 10 + .../MareConfiguration/NotesConfigService.cs | 14 + .../PlayerPerformanceConfigService.cs | 11 + .../RemoteConfigCacheService.cs | 11 + .../ServerBlockConfigService.cs | 14 + .../MareConfiguration/ServerConfigService.cs | 14 + .../ServerTagConfigService.cs | 14 + .../SyncshellConfigService.cs | 14 + .../TransientConfigService.cs | 14 + .../XivDataStorageService.cs | 12 + MareSynchronos/MarePlugin.cs | 170 + MareSynchronos/MareSynchronos.csproj | 63 + .../PlayerData/Data/CharacterData.cs | 50 + .../PlayerData/Data/FileReplacement.cs | 42 + .../Data/FileReplacementComparer.cs | 47 + .../Data/FileReplacementDataComparer.cs | 49 + .../PlayerData/Data/PlayerChanges.cs | 14 + .../Factories/FileDownloadManagerFactory.cs | 30 + .../Factories/GameObjectHandlerFactory.cs | 30 + .../Factories/PairAnalyzerFactory.cs | 30 + .../PlayerData/Factories/PairFactory.cs | 33 + .../Factories/PairHandlerFactory.cs | 60 + .../PlayerData/Factories/PlayerDataFactory.cs | 365 ++ .../PlayerData/Handlers/GameObjectHandler.cs | 487 +++ .../PlayerData/Handlers/PairHandler.cs | 890 ++++ .../PlayerData/Pairs/OnlinePlayerManager.cs | 75 + .../PlayerData/Pairs/OptionalPluginWarning.cs | 10 + MareSynchronos/PlayerData/Pairs/Pair.cs | 371 ++ .../PlayerData/Pairs/PairManager.cs | 403 ++ .../Services/CacheCreationService.cs | 263 ++ MareSynchronos/Plugin.cs | 230 ++ .../CharaData/CharaDataCharacterHandler.cs | 133 + .../CharaData/CharaDataFileHandler.cs | 302 ++ .../CharaDataGposeTogetherManager.cs | 696 ++++ .../Services/CharaData/CharaDataManager.cs | 1022 +++++ .../CharaData/CharaDataNearbyManager.cs | 296 ++ .../CharaData/MareCharaFileDataFactory.cs | 20 + .../Models/CharaDataExtendedUpdateDto.cs | 362 ++ .../Models/CharaDataFullExtendedDto.cs | 18 + .../Models/CharaDataMetaInfoExtendedDto.cs | 31 + .../CharaData/Models/GposeLobbyUserData.cs | 174 + .../CharaData/Models/HandledCharaDataEntry.cs | 6 + .../CharaData/Models/MareCharaFileData.cs | 70 + .../CharaData/Models/MareCharaFileHeader.cs | 54 + .../CharaData/Models/PoseEntryExtended.cs | 75 + MareSynchronos/Services/CharacterAnalyzer.cs | 242 ++ MareSynchronos/Services/ChatService.cs | 241 ++ .../Services/CommandManagerService.cs | 155 + MareSynchronos/Services/DalamudUtilService.cs | 794 ++++ MareSynchronos/Services/Events/Event.cs | 45 + .../Services/Events/EventAggregator.cs | 113 + .../Services/Events/EventSeverity.cs | 8 + MareSynchronos/Services/GuiHookService.cs | 144 + MareSynchronos/Services/MareProfileData.cs | 6 + MareSynchronos/Services/MareProfileManager.cs | 78 + .../DisposableMediatorSubscriberBase.cs | 22 + .../Services/Mediator/IMediatorSubscriber.cs | 6 + .../Services/Mediator/MareMediator.cs | 222 + .../Mediator/MediatorSubscriberBase.cs | 23 + .../Services/Mediator/MessageBase.cs | 20 + MareSynchronos/Services/Mediator/Messages.cs | 113 + .../Mediator/WindowMediatorSubscriberBase.cs | 54 + .../Services/NotificationService.cs | 141 + MareSynchronos/Services/PairAnalyzer.cs | 214 + .../Services/PerformanceCollectorService.cs | 199 + .../Services/PlayerPerformanceService.cs | 330 ++ .../PluginWarningNotificationService.cs | 76 + .../Services/PluginWatcherService.cs | 160 + .../ServerConfigurationManager.cs | 547 +++ MareSynchronos/Services/UiFactory.cs | 60 + MareSynchronos/Services/UiService.cs | 137 + MareSynchronos/Services/VisibilityService.cs | 105 + MareSynchronos/Services/XivDataAnalyzer.cs | 257 ++ MareSynchronos/Snowcloak.json | 14 + MareSynchronos/UI/CharaDataHubUi.Functions.cs | 196 + .../UI/CharaDataHubUi.GposeTogether.cs | 227 ++ MareSynchronos/UI/CharaDataHubUi.McdOnline.cs | 851 ++++ .../UI/CharaDataHubUi.NearbyPoses.cs | 207 + MareSynchronos/UI/CharaDataHubUi.cs | 1107 +++++ MareSynchronos/UI/CompactUI.cs | 640 +++ MareSynchronos/UI/Components/DrawGroupPair.cs | 376 ++ MareSynchronos/UI/Components/DrawPairBase.cs | 65 + MareSynchronos/UI/Components/DrawUserPair.cs | 306 ++ MareSynchronos/UI/Components/GroupPanel.cs | 703 ++++ MareSynchronos/UI/Components/PairGroupsUi.cs | 258 ++ .../Components/Popup/BanUserPopupHandler.cs | 50 + .../UI/Components/Popup/IPopupHandler.cs | 11 + .../UI/Components/Popup/PopupHandler.cs | 81 + .../UI/Components/Popup/ReportPopupHandler.cs | 58 + .../UI/Components/SelectGroupForPairUi.cs | 139 + .../UI/Components/SelectPairForGroupUi.cs | 92 + MareSynchronos/UI/DataAnalysisUi.cs | 492 +++ MareSynchronos/UI/DownloadUi.cs | 248 ++ MareSynchronos/UI/DtrEntry.cs | 241 ++ MareSynchronos/UI/EditProfileUi.cs | 220 + MareSynchronos/UI/EventViewerUI.cs | 238 ++ MareSynchronos/UI/Handlers/TagHandler.cs | 85 + .../UI/Handlers/UidDisplayHandler.cs | 204 + MareSynchronos/UI/IntroUI.cs | 380 ++ MareSynchronos/UI/PermissionWindowUI.cs | 167 + MareSynchronos/UI/PlayerAnalysisUI.cs | 366 ++ MareSynchronos/UI/PopoutProfileUi.cs | 185 + MareSynchronos/UI/SettingsUi.cs | 1922 +++++++++ MareSynchronos/UI/StandaloneProfileUi.cs | 167 + MareSynchronos/UI/SyncshellAdminUI.cs | 455 +++ MareSynchronos/UI/UISharedService.cs | 1015 +++++ MareSynchronos/Utils/ChatUtils.cs | 34 + MareSynchronos/Utils/Crypto.cs | 28 + MareSynchronos/Utils/HashingStream.cs | 80 + MareSynchronos/Utils/LimitedStream.cs | 128 + .../Utils/MareInterpolatedStringHandler.cs | 27 + MareSynchronos/Utils/PngHdr.cs | 57 + MareSynchronos/Utils/RollingList.cs | 47 + MareSynchronos/Utils/ValueProgress.cs | 22 + MareSynchronos/Utils/VariousExtensions.cs | 230 ++ .../WebAPI/AccountRegistrationService.cs | 70 + .../WebAPI/Files/FileDownloadManager.cs | 510 +++ .../WebAPI/Files/FileTransferOrchestrator.cs | 177 + .../WebAPI/Files/FileUploadManager.cs | 289 ++ .../Files/Models/DownloadFileTransfer.cs | 24 + .../WebAPI/Files/Models/DownloadStatus.cs | 10 + .../WebAPI/Files/Models/FileDownloadStatus.cs | 10 + .../WebAPI/Files/Models/FileTransfer.cs | 27 + .../Files/Models/ProgressableStreamContent.cs | 93 + .../WebAPI/Files/Models/UploadFileTransfer.cs | 13 + .../WebAPI/Files/Models/UploadProgress.cs | 3 + .../WebAPI/Files/ThrottledStream.cs | 231 ++ .../SignalR/ApIController.Functions.Users.cs | 116 + .../ApiController.Functions.Callbacks.cs | 405 ++ .../ApiController.Functions.CharaData.cs | 228 ++ .../SignalR/ApiController.Functions.Groups.cs | 128 + .../WebAPI/SignalR/ApiController.cs | 482 +++ .../WebAPI/SignalR/HubConnectionConfig.cs | 50 + MareSynchronos/WebAPI/SignalR/HubFactory.cs | 228 ++ .../WebAPI/SignalR/JwtIdentifier.cs | 9 + .../SignalR/MareAuthFailureException.cs | 11 + .../WebAPI/SignalR/TokenProvider.cs | 183 + .../SignalR/Utils/ForeverRetryPolicy.cs | 39 + .../WebAPI/SignalR/Utils/ServerState.cs | 16 + MareSynchronos/packages.lock.json | 533 +++ Penumbra.Api/.editorconfig | 3625 +++++++++++++++++ Penumbra.Api/.gitignore | 3 + Penumbra.Api/Api/Collection.cs | 66 + Penumbra.Api/Api/Editing.cs | 28 + Penumbra.Api/Api/GameState.cs | 52 + Penumbra.Api/Api/IPenumbraApi.cs | 41 + Penumbra.Api/Api/IPenumbraApiBase.cs | 16 + Penumbra.Api/Api/Meta.cs | 13 + Penumbra.Api/Api/ModSettings.cs | 77 + Penumbra.Api/Api/Mods.cs | 78 + Penumbra.Api/Api/PluginState.cs | 26 + Penumbra.Api/Api/Redraw.cs | 23 + Penumbra.Api/Api/Resolve.cs | 64 + Penumbra.Api/Api/ResourceTree.cs | 73 + Penumbra.Api/Api/Temporary.cs | 147 + Penumbra.Api/Api/Ui.cs | 59 + Penumbra.Api/Delegates.cs | 42 + Penumbra.Api/Enums/ApiCollectionType.cs | 97 + Penumbra.Api/Enums/ChangedItemIcon.cs | 23 + Penumbra.Api/Enums/ChangedItemType.cs | 17 + Penumbra.Api/Enums/GroupType.cs | 39 + Penumbra.Api/Enums/ModSettingChange.cs | 34 + Penumbra.Api/Enums/MouseButton.cs | 12 + Penumbra.Api/Enums/PenumbraApiEc.cs | 33 + Penumbra.Api/Enums/RedrawType.cs | 11 + Penumbra.Api/Enums/ResourceType.cs | 79 + Penumbra.Api/Enums/TabType.cs | 19 + Penumbra.Api/Enums/TextureType.cs | 37 + Penumbra.Api/GlobalUsings.cs | 5 + Penumbra.Api/Helpers/ActionProvider.cs | 136 + Penumbra.Api/Helpers/ActionSubscriber.cs | 114 + Penumbra.Api/Helpers/ConvertingDict.cs | 152 + Penumbra.Api/Helpers/DtoClasses.cs | 53 + Penumbra.Api/Helpers/EventProvider.cs | 465 +++ Penumbra.Api/Helpers/EventSubscriber.cs | 582 +++ Penumbra.Api/Helpers/FuncProvider.cs | 223 + Penumbra.Api/Helpers/FuncSubscriber.cs | 217 + Penumbra.Api/Helpers/PluginLogHelper.cs | 26 + Penumbra.Api/IpcSubscribers/Collection.cs | 154 + Penumbra.Api/IpcSubscribers/Editing.cs | 38 + Penumbra.Api/IpcSubscribers/GameState.cs | 100 + .../IpcSubscribers/Legacy/Collection.cs | 99 + .../IpcSubscribers/Legacy/GameState.cs | 43 + Penumbra.Api/IpcSubscribers/Legacy/Meta.cs | 24 + .../IpcSubscribers/Legacy/ModSettings.cs | 92 + Penumbra.Api/IpcSubscribers/Legacy/Mods.cs | 70 + .../IpcSubscribers/Legacy/PluginState.cs | 15 + Penumbra.Api/IpcSubscribers/Legacy/Redraw.cs | 44 + Penumbra.Api/IpcSubscribers/Legacy/Resolve.cs | 24 + .../IpcSubscribers/Legacy/ResourceTree.cs | 83 + .../IpcSubscribers/Legacy/Temporary.cs | 88 + Penumbra.Api/IpcSubscribers/Legacy/Ui.cs | 16 + Penumbra.Api/IpcSubscribers/Meta.cs | 37 + Penumbra.Api/IpcSubscribers/ModSettings.cs | 218 + Penumbra.Api/IpcSubscribers/Mods.cs | 218 + Penumbra.Api/IpcSubscribers/PluginState.cs | 128 + Penumbra.Api/IpcSubscribers/Redraw.cs | 53 + Penumbra.Api/IpcSubscribers/Resolve.cs | 133 + Penumbra.Api/IpcSubscribers/ResourceTree.cs | 116 + Penumbra.Api/IpcSubscribers/Temporary.cs | 276 ++ Penumbra.Api/IpcSubscribers/Ui.cs | 130 + Penumbra.Api/Penumbra.Api.csproj | 34 + Penumbra.Api/Penumbra.Api.csproj.DotSettings | 2 + Penumbra.Api/README.md | 4 + Penumbra.Api/packages.lock.json | 13 + README.md | 13 + 366 files changed, 51109 insertions(+) create mode 100644 Glamourer.Api/.editorconfig create mode 100644 Glamourer.Api/.gitignore create mode 100644 Glamourer.Api/Api/IGlamourerApi.cs create mode 100644 Glamourer.Api/Api/IGlamourerApiBase.cs create mode 100644 Glamourer.Api/Api/IGlamourerApiDesigns.cs create mode 100644 Glamourer.Api/Api/IGlamourerApiItems.cs create mode 100644 Glamourer.Api/Api/IGlamourerApiState.cs create mode 100644 Glamourer.Api/Enums/ApiBonusSlot.cs create mode 100644 Glamourer.Api/Enums/ApiEquipSlot.cs create mode 100644 Glamourer.Api/Enums/ApplyFlag.cs create mode 100644 Glamourer.Api/Enums/GlamourerApiEc.cs create mode 100644 Glamourer.Api/Enums/SetMetaFlag.cs create mode 100644 Glamourer.Api/Enums/StateChangeType.cs create mode 100644 Glamourer.Api/Enums/StateFinalizationType.cs create mode 100644 Glamourer.Api/Glamourer.Api.csproj create mode 100644 Glamourer.Api/Glamourer.Api.csproj.DotSettings create mode 100644 Glamourer.Api/GlobalUsings.cs create mode 100644 Glamourer.Api/Helpers/ActionSubscriber.cs create mode 100644 Glamourer.Api/Helpers/EventProvider.cs create mode 100644 Glamourer.Api/Helpers/EventSubscriber.cs create mode 100644 Glamourer.Api/Helpers/FuncProvider.cs create mode 100644 Glamourer.Api/Helpers/FuncSubscriber.cs create mode 100644 Glamourer.Api/Helpers/PluginLogHelper.cs create mode 100644 Glamourer.Api/IpcSubscribers/Designs.cs create mode 100644 Glamourer.Api/IpcSubscribers/Items.cs create mode 100644 Glamourer.Api/IpcSubscribers/Legacy/Designs.cs create mode 100644 Glamourer.Api/IpcSubscribers/Legacy/Items.cs create mode 100644 Glamourer.Api/IpcSubscribers/Legacy/PluginState.cs create mode 100644 Glamourer.Api/IpcSubscribers/Legacy/State.cs create mode 100644 Glamourer.Api/IpcSubscribers/PluginState.cs create mode 100644 Glamourer.Api/IpcSubscribers/State.cs create mode 100644 Glamourer.Api/README.md create mode 100644 Glamourer.Api/packages.lock.json create mode 100644 LICENSE create mode 100644 LICENSE_MIT create mode 100644 MareAPI/.gitignore create mode 100644 MareAPI/LICENSE create mode 100644 MareAPI/MareSynchronosAPI/Data/CharacterData.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/ChatMessage.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Comparer/UserDataComparer.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Enum/GroupPermissions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Enum/GroupUserInfo.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Enum/MessageSeverity.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Enum/ObjectKind.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Enum/UserPermissions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Extensions/GroupPermissionsExtensions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserPermissionsExtensions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/Extensions/UserPermissionsExtensions.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/FileReplacementData.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/GroupData.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/SignedChatMessage.cs create mode 100644 MareAPI/MareSynchronosAPI/Data/UserData.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/ConnectionDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Files/DownloadFileDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Files/FilesSendDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Files/ITransferFileDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Files/UploadFileDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupInfoDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupPairDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserPermissionDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/SystemInfoDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/UserDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/UserPairDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/UserPermissionsDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/UserProfileDto.cs create mode 100644 MareAPI/MareSynchronosAPI/Dto/User/UserProfileReportDto.cs create mode 100644 MareAPI/MareSynchronosAPI/MareSynchronos.API.csproj create mode 100644 MareAPI/MareSynchronosAPI/MareSynchronosAPI.sln create mode 100644 MareAPI/MareSynchronosAPI/Routes/MareAuth.cs create mode 100644 MareAPI/MareSynchronosAPI/Routes/MareFiles.cs create mode 100644 MareAPI/MareSynchronosAPI/SignalR/IMareHub.cs create mode 100644 MareAPI/MareSynchronosAPI/SignalR/IMareHubClient.cs create mode 100644 MareSynchronos.sln create mode 100644 MareSynchronos/.editorconfig create mode 100644 MareSynchronos/FileCache/CacheMonitor.cs create mode 100644 MareSynchronos/FileCache/FileCacheEntity.cs create mode 100644 MareSynchronos/FileCache/FileCacheManager.cs create mode 100644 MareSynchronos/FileCache/FileCompactor.cs create mode 100644 MareSynchronos/FileCache/FileState.cs create mode 100644 MareSynchronos/FileCache/TransientResourceManager.cs create mode 100644 MareSynchronos/GlobalSuppressions.cs create mode 100644 MareSynchronos/Interop/BlockedCharacterHandler.cs create mode 100644 MareSynchronos/Interop/DalamudLogger.cs create mode 100644 MareSynchronos/Interop/DalamudLoggingProvider.cs create mode 100644 MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs create mode 100644 MareSynchronos/Interop/GameChatHooks.cs create mode 100644 MareSynchronos/Interop/GameModel/MdlFile.cs create mode 100644 MareSynchronos/Interop/Ipc/IIpcCaller.cs create mode 100644 MareSynchronos/Interop/Ipc/IpcCallerBrio.cs create mode 100644 MareSynchronos/Interop/Ipc/IpcCallerCustomize.cs create mode 100644 MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs create mode 100644 MareSynchronos/Interop/Ipc/IpcCallerHeels.cs create mode 100644 MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs create mode 100644 MareSynchronos/Interop/Ipc/IpcCallerMare.cs create mode 100644 MareSynchronos/Interop/Ipc/IpcCallerMoodles.cs create mode 100644 MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs create mode 100644 MareSynchronos/Interop/Ipc/IpcCallerPetNames.cs create mode 100644 MareSynchronos/Interop/Ipc/IpcManager.cs create mode 100644 MareSynchronos/Interop/Ipc/IpcProvider.cs create mode 100644 MareSynchronos/Interop/Ipc/RedrawManager.cs create mode 100644 MareSynchronos/Interop/VfxSpawnManager.cs create mode 100644 MareSynchronos/MareConfiguration/CharaDataConfigService.cs create mode 100644 MareSynchronos/MareConfiguration/ConfigurationExtensions.cs create mode 100644 MareSynchronos/MareConfiguration/ConfigurationMigrator.cs create mode 100644 MareSynchronos/MareConfiguration/ConfigurationSaveService.cs create mode 100644 MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs create mode 100644 MareSynchronos/MareConfiguration/Configurations/CharaDataConfig.cs create mode 100644 MareSynchronos/MareConfiguration/Configurations/IMareConfiguration.cs create mode 100644 MareSynchronos/MareConfiguration/Configurations/MareConfig.cs create mode 100644 MareSynchronos/MareConfiguration/Configurations/PlayerPerformanceConfig.cs create mode 100644 MareSynchronos/MareConfiguration/Configurations/RemoteConfigCache.cs create mode 100644 MareSynchronos/MareConfiguration/Configurations/ServerBlockConfig.cs create mode 100644 MareSynchronos/MareConfiguration/Configurations/ServerConfig.cs create mode 100644 MareSynchronos/MareConfiguration/Configurations/ServerTagConfig.cs create mode 100644 MareSynchronos/MareConfiguration/Configurations/SyncshellConfig.cs create mode 100644 MareSynchronos/MareConfiguration/Configurations/TransientConfig.cs create mode 100644 MareSynchronos/MareConfiguration/Configurations/UidNotesConfig.cs create mode 100644 MareSynchronos/MareConfiguration/Configurations/XivDataStorageConfig.cs create mode 100644 MareSynchronos/MareConfiguration/IConfigService.cs create mode 100644 MareSynchronos/MareConfiguration/MareConfigService.cs create mode 100644 MareSynchronos/MareConfiguration/Models/Authentication.cs create mode 100644 MareSynchronos/MareConfiguration/Models/CharaDataFavorite.cs create mode 100644 MareSynchronos/MareConfiguration/Models/DownloadSpeeds.cs create mode 100644 MareSynchronos/MareConfiguration/Models/NotificationLocation.cs create mode 100644 MareSynchronos/MareConfiguration/Models/Obsolete/ServerStorageV0.cs create mode 100644 MareSynchronos/MareConfiguration/Models/SecretKey.cs create mode 100644 MareSynchronos/MareConfiguration/Models/ServerBlockStorage.cs create mode 100644 MareSynchronos/MareConfiguration/Models/ServerNotesStorage.cs create mode 100644 MareSynchronos/MareConfiguration/Models/ServerShellStorage.cs create mode 100644 MareSynchronos/MareConfiguration/Models/ServerStorage.cs create mode 100644 MareSynchronos/MareConfiguration/Models/ServerTagStorage.cs create mode 100644 MareSynchronos/MareConfiguration/Models/ShellConfig.cs create mode 100644 MareSynchronos/MareConfiguration/Models/TextureShrinkMode.cs create mode 100644 MareSynchronos/MareConfiguration/NotesConfigService.cs create mode 100644 MareSynchronos/MareConfiguration/PlayerPerformanceConfigService.cs create mode 100644 MareSynchronos/MareConfiguration/RemoteConfigCacheService.cs create mode 100644 MareSynchronos/MareConfiguration/ServerBlockConfigService.cs create mode 100644 MareSynchronos/MareConfiguration/ServerConfigService.cs create mode 100644 MareSynchronos/MareConfiguration/ServerTagConfigService.cs create mode 100644 MareSynchronos/MareConfiguration/SyncshellConfigService.cs create mode 100644 MareSynchronos/MareConfiguration/TransientConfigService.cs create mode 100644 MareSynchronos/MareConfiguration/XivDataStorageService.cs create mode 100644 MareSynchronos/MarePlugin.cs create mode 100644 MareSynchronos/MareSynchronos.csproj create mode 100644 MareSynchronos/PlayerData/Data/CharacterData.cs create mode 100644 MareSynchronos/PlayerData/Data/FileReplacement.cs create mode 100644 MareSynchronos/PlayerData/Data/FileReplacementComparer.cs create mode 100644 MareSynchronos/PlayerData/Data/FileReplacementDataComparer.cs create mode 100644 MareSynchronos/PlayerData/Data/PlayerChanges.cs create mode 100644 MareSynchronos/PlayerData/Factories/FileDownloadManagerFactory.cs create mode 100644 MareSynchronos/PlayerData/Factories/GameObjectHandlerFactory.cs create mode 100644 MareSynchronos/PlayerData/Factories/PairAnalyzerFactory.cs create mode 100644 MareSynchronos/PlayerData/Factories/PairFactory.cs create mode 100644 MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs create mode 100644 MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs create mode 100644 MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs create mode 100644 MareSynchronos/PlayerData/Handlers/PairHandler.cs create mode 100644 MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs create mode 100644 MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs create mode 100644 MareSynchronos/PlayerData/Pairs/Pair.cs create mode 100644 MareSynchronos/PlayerData/Pairs/PairManager.cs create mode 100644 MareSynchronos/PlayerData/Services/CacheCreationService.cs create mode 100644 MareSynchronos/Plugin.cs create mode 100644 MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs create mode 100644 MareSynchronos/Services/CharaData/CharaDataFileHandler.cs create mode 100644 MareSynchronos/Services/CharaData/CharaDataGposeTogetherManager.cs create mode 100644 MareSynchronos/Services/CharaData/CharaDataManager.cs create mode 100644 MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs create mode 100644 MareSynchronos/Services/CharaData/MareCharaFileDataFactory.cs create mode 100644 MareSynchronos/Services/CharaData/Models/CharaDataExtendedUpdateDto.cs create mode 100644 MareSynchronos/Services/CharaData/Models/CharaDataFullExtendedDto.cs create mode 100644 MareSynchronos/Services/CharaData/Models/CharaDataMetaInfoExtendedDto.cs create mode 100644 MareSynchronos/Services/CharaData/Models/GposeLobbyUserData.cs create mode 100644 MareSynchronos/Services/CharaData/Models/HandledCharaDataEntry.cs create mode 100644 MareSynchronos/Services/CharaData/Models/MareCharaFileData.cs create mode 100644 MareSynchronos/Services/CharaData/Models/MareCharaFileHeader.cs create mode 100644 MareSynchronos/Services/CharaData/Models/PoseEntryExtended.cs create mode 100644 MareSynchronos/Services/CharacterAnalyzer.cs create mode 100644 MareSynchronos/Services/ChatService.cs create mode 100644 MareSynchronos/Services/CommandManagerService.cs create mode 100644 MareSynchronos/Services/DalamudUtilService.cs create mode 100644 MareSynchronos/Services/Events/Event.cs create mode 100644 MareSynchronos/Services/Events/EventAggregator.cs create mode 100644 MareSynchronos/Services/Events/EventSeverity.cs create mode 100644 MareSynchronos/Services/GuiHookService.cs create mode 100644 MareSynchronos/Services/MareProfileData.cs create mode 100644 MareSynchronos/Services/MareProfileManager.cs create mode 100644 MareSynchronos/Services/Mediator/DisposableMediatorSubscriberBase.cs create mode 100644 MareSynchronos/Services/Mediator/IMediatorSubscriber.cs create mode 100644 MareSynchronos/Services/Mediator/MareMediator.cs create mode 100644 MareSynchronos/Services/Mediator/MediatorSubscriberBase.cs create mode 100644 MareSynchronos/Services/Mediator/MessageBase.cs create mode 100644 MareSynchronos/Services/Mediator/Messages.cs create mode 100644 MareSynchronos/Services/Mediator/WindowMediatorSubscriberBase.cs create mode 100644 MareSynchronos/Services/NotificationService.cs create mode 100644 MareSynchronos/Services/PairAnalyzer.cs create mode 100644 MareSynchronos/Services/PerformanceCollectorService.cs create mode 100644 MareSynchronos/Services/PlayerPerformanceService.cs create mode 100644 MareSynchronos/Services/PluginWarningNotificationService.cs create mode 100644 MareSynchronos/Services/PluginWatcherService.cs create mode 100644 MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs create mode 100644 MareSynchronos/Services/UiFactory.cs create mode 100644 MareSynchronos/Services/UiService.cs create mode 100644 MareSynchronos/Services/VisibilityService.cs create mode 100644 MareSynchronos/Services/XivDataAnalyzer.cs create mode 100644 MareSynchronos/Snowcloak.json create mode 100644 MareSynchronos/UI/CharaDataHubUi.Functions.cs create mode 100644 MareSynchronos/UI/CharaDataHubUi.GposeTogether.cs create mode 100644 MareSynchronos/UI/CharaDataHubUi.McdOnline.cs create mode 100644 MareSynchronos/UI/CharaDataHubUi.NearbyPoses.cs create mode 100644 MareSynchronos/UI/CharaDataHubUi.cs create mode 100644 MareSynchronos/UI/CompactUI.cs create mode 100644 MareSynchronos/UI/Components/DrawGroupPair.cs create mode 100644 MareSynchronos/UI/Components/DrawPairBase.cs create mode 100644 MareSynchronos/UI/Components/DrawUserPair.cs create mode 100644 MareSynchronos/UI/Components/GroupPanel.cs create mode 100644 MareSynchronos/UI/Components/PairGroupsUi.cs create mode 100644 MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs create mode 100644 MareSynchronos/UI/Components/Popup/IPopupHandler.cs create mode 100644 MareSynchronos/UI/Components/Popup/PopupHandler.cs create mode 100644 MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs create mode 100644 MareSynchronos/UI/Components/SelectGroupForPairUi.cs create mode 100644 MareSynchronos/UI/Components/SelectPairForGroupUi.cs create mode 100644 MareSynchronos/UI/DataAnalysisUi.cs create mode 100644 MareSynchronos/UI/DownloadUi.cs create mode 100644 MareSynchronos/UI/DtrEntry.cs create mode 100644 MareSynchronos/UI/EditProfileUi.cs create mode 100644 MareSynchronos/UI/EventViewerUI.cs create mode 100644 MareSynchronos/UI/Handlers/TagHandler.cs create mode 100644 MareSynchronos/UI/Handlers/UidDisplayHandler.cs create mode 100644 MareSynchronos/UI/IntroUI.cs create mode 100644 MareSynchronos/UI/PermissionWindowUI.cs create mode 100644 MareSynchronos/UI/PlayerAnalysisUI.cs create mode 100644 MareSynchronos/UI/PopoutProfileUi.cs create mode 100644 MareSynchronos/UI/SettingsUi.cs create mode 100644 MareSynchronos/UI/StandaloneProfileUi.cs create mode 100644 MareSynchronos/UI/SyncshellAdminUI.cs create mode 100644 MareSynchronos/UI/UISharedService.cs create mode 100644 MareSynchronos/Utils/ChatUtils.cs create mode 100644 MareSynchronos/Utils/Crypto.cs create mode 100644 MareSynchronos/Utils/HashingStream.cs create mode 100644 MareSynchronos/Utils/LimitedStream.cs create mode 100644 MareSynchronos/Utils/MareInterpolatedStringHandler.cs create mode 100644 MareSynchronos/Utils/PngHdr.cs create mode 100644 MareSynchronos/Utils/RollingList.cs create mode 100644 MareSynchronos/Utils/ValueProgress.cs create mode 100644 MareSynchronos/Utils/VariousExtensions.cs create mode 100644 MareSynchronos/WebAPI/AccountRegistrationService.cs create mode 100644 MareSynchronos/WebAPI/Files/FileDownloadManager.cs create mode 100644 MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs create mode 100644 MareSynchronos/WebAPI/Files/FileUploadManager.cs create mode 100644 MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs create mode 100644 MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs create mode 100644 MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs create mode 100644 MareSynchronos/WebAPI/Files/Models/FileTransfer.cs create mode 100644 MareSynchronos/WebAPI/Files/Models/ProgressableStreamContent.cs create mode 100644 MareSynchronos/WebAPI/Files/Models/UploadFileTransfer.cs create mode 100644 MareSynchronos/WebAPI/Files/Models/UploadProgress.cs create mode 100644 MareSynchronos/WebAPI/Files/ThrottledStream.cs create mode 100644 MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs create mode 100644 MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs create mode 100644 MareSynchronos/WebAPI/SignalR/ApiController.Functions.CharaData.cs create mode 100644 MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs create mode 100644 MareSynchronos/WebAPI/SignalR/ApiController.cs create mode 100644 MareSynchronos/WebAPI/SignalR/HubConnectionConfig.cs create mode 100644 MareSynchronos/WebAPI/SignalR/HubFactory.cs create mode 100644 MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs create mode 100644 MareSynchronos/WebAPI/SignalR/MareAuthFailureException.cs create mode 100644 MareSynchronos/WebAPI/SignalR/TokenProvider.cs create mode 100644 MareSynchronos/WebAPI/SignalR/Utils/ForeverRetryPolicy.cs create mode 100644 MareSynchronos/WebAPI/SignalR/Utils/ServerState.cs create mode 100644 MareSynchronos/packages.lock.json create mode 100644 Penumbra.Api/.editorconfig create mode 100644 Penumbra.Api/.gitignore create mode 100644 Penumbra.Api/Api/Collection.cs create mode 100644 Penumbra.Api/Api/Editing.cs create mode 100644 Penumbra.Api/Api/GameState.cs create mode 100644 Penumbra.Api/Api/IPenumbraApi.cs create mode 100644 Penumbra.Api/Api/IPenumbraApiBase.cs create mode 100644 Penumbra.Api/Api/Meta.cs create mode 100644 Penumbra.Api/Api/ModSettings.cs create mode 100644 Penumbra.Api/Api/Mods.cs create mode 100644 Penumbra.Api/Api/PluginState.cs create mode 100644 Penumbra.Api/Api/Redraw.cs create mode 100644 Penumbra.Api/Api/Resolve.cs create mode 100644 Penumbra.Api/Api/ResourceTree.cs create mode 100644 Penumbra.Api/Api/Temporary.cs create mode 100644 Penumbra.Api/Api/Ui.cs create mode 100644 Penumbra.Api/Delegates.cs create mode 100644 Penumbra.Api/Enums/ApiCollectionType.cs create mode 100644 Penumbra.Api/Enums/ChangedItemIcon.cs create mode 100644 Penumbra.Api/Enums/ChangedItemType.cs create mode 100644 Penumbra.Api/Enums/GroupType.cs create mode 100644 Penumbra.Api/Enums/ModSettingChange.cs create mode 100644 Penumbra.Api/Enums/MouseButton.cs create mode 100644 Penumbra.Api/Enums/PenumbraApiEc.cs create mode 100644 Penumbra.Api/Enums/RedrawType.cs create mode 100644 Penumbra.Api/Enums/ResourceType.cs create mode 100644 Penumbra.Api/Enums/TabType.cs create mode 100644 Penumbra.Api/Enums/TextureType.cs create mode 100644 Penumbra.Api/GlobalUsings.cs create mode 100644 Penumbra.Api/Helpers/ActionProvider.cs create mode 100644 Penumbra.Api/Helpers/ActionSubscriber.cs create mode 100644 Penumbra.Api/Helpers/ConvertingDict.cs create mode 100644 Penumbra.Api/Helpers/DtoClasses.cs create mode 100644 Penumbra.Api/Helpers/EventProvider.cs create mode 100644 Penumbra.Api/Helpers/EventSubscriber.cs create mode 100644 Penumbra.Api/Helpers/FuncProvider.cs create mode 100644 Penumbra.Api/Helpers/FuncSubscriber.cs create mode 100644 Penumbra.Api/Helpers/PluginLogHelper.cs create mode 100644 Penumbra.Api/IpcSubscribers/Collection.cs create mode 100644 Penumbra.Api/IpcSubscribers/Editing.cs create mode 100644 Penumbra.Api/IpcSubscribers/GameState.cs create mode 100644 Penumbra.Api/IpcSubscribers/Legacy/Collection.cs create mode 100644 Penumbra.Api/IpcSubscribers/Legacy/GameState.cs create mode 100644 Penumbra.Api/IpcSubscribers/Legacy/Meta.cs create mode 100644 Penumbra.Api/IpcSubscribers/Legacy/ModSettings.cs create mode 100644 Penumbra.Api/IpcSubscribers/Legacy/Mods.cs create mode 100644 Penumbra.Api/IpcSubscribers/Legacy/PluginState.cs create mode 100644 Penumbra.Api/IpcSubscribers/Legacy/Redraw.cs create mode 100644 Penumbra.Api/IpcSubscribers/Legacy/Resolve.cs create mode 100644 Penumbra.Api/IpcSubscribers/Legacy/ResourceTree.cs create mode 100644 Penumbra.Api/IpcSubscribers/Legacy/Temporary.cs create mode 100644 Penumbra.Api/IpcSubscribers/Legacy/Ui.cs create mode 100644 Penumbra.Api/IpcSubscribers/Meta.cs create mode 100644 Penumbra.Api/IpcSubscribers/ModSettings.cs create mode 100644 Penumbra.Api/IpcSubscribers/Mods.cs create mode 100644 Penumbra.Api/IpcSubscribers/PluginState.cs create mode 100644 Penumbra.Api/IpcSubscribers/Redraw.cs create mode 100644 Penumbra.Api/IpcSubscribers/Resolve.cs create mode 100644 Penumbra.Api/IpcSubscribers/ResourceTree.cs create mode 100644 Penumbra.Api/IpcSubscribers/Temporary.cs create mode 100644 Penumbra.Api/IpcSubscribers/Ui.cs create mode 100644 Penumbra.Api/Penumbra.Api.csproj create mode 100644 Penumbra.Api/Penumbra.Api.csproj.DotSettings create mode 100644 Penumbra.Api/README.md create mode 100644 Penumbra.Api/packages.lock.json create mode 100644 README.md diff --git a/Glamourer.Api/.editorconfig b/Glamourer.Api/.editorconfig new file mode 100644 index 0000000..e3be45d --- /dev/null +++ b/Glamourer.Api/.editorconfig @@ -0,0 +1,3625 @@ +# Standard properties +charset = utf-8 +end_of_line = lf +insert_final_newline = true +csharp_indent_labels = one_less_than_current +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_prefer_top_level_statements = true:silent + +[*] +# Microsoft .NET properties +csharp_indent_braces=false +csharp_indent_switch_labels=true +csharp_new_line_before_catch=true +csharp_new_line_before_else=true +csharp_new_line_before_finally=true +csharp_new_line_before_members_in_object_initializers=true +csharp_new_line_before_open_brace=all +csharp_new_line_between_query_expression_clauses=true +csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_preserve_single_line_blocks=true +csharp_space_after_cast=false +csharp_space_after_colon_in_inheritance_clause=true +csharp_space_after_comma=true +csharp_space_after_dot=false +csharp_space_after_keywords_in_control_flow_statements=true +csharp_space_after_semicolon_in_for_statement=true +csharp_space_around_binary_operators=before_and_after +csharp_space_before_colon_in_inheritance_clause=true +csharp_space_before_comma=false +csharp_space_before_dot=false +csharp_space_before_open_square_brackets=false +csharp_space_before_semicolon_in_for_statement=false +csharp_space_between_empty_square_brackets=false +csharp_space_between_method_call_empty_parameter_list_parentheses=false +csharp_space_between_method_call_name_and_opening_parenthesis=false +csharp_space_between_method_call_parameter_list_parentheses=false +csharp_space_between_method_declaration_empty_parameter_list_parentheses=false +csharp_space_between_method_declaration_name_and_open_parenthesis=false +csharp_space_between_method_declaration_parameter_list_parentheses=false +csharp_space_between_parentheses=false +csharp_space_between_square_brackets=false +csharp_style_namespace_declarations= file_scoped:suggestion +csharp_style_var_elsewhere=true:suggestion +csharp_style_var_for_built_in_types=true:suggestion +csharp_style_var_when_type_is_apparent=true:suggestion +csharp_using_directive_placement= outside_namespace:silent +dotnet_diagnostic.bc40000.severity=warning +dotnet_diagnostic.bc400005.severity=warning +dotnet_diagnostic.bc40008.severity=warning +dotnet_diagnostic.bc40056.severity=warning +dotnet_diagnostic.bc42016.severity=warning +dotnet_diagnostic.bc42024.severity=warning +dotnet_diagnostic.bc42025.severity=warning +dotnet_diagnostic.bc42104.severity=warning +dotnet_diagnostic.bc42105.severity=warning +dotnet_diagnostic.bc42106.severity=warning +dotnet_diagnostic.bc42107.severity=warning +dotnet_diagnostic.bc42304.severity=warning +dotnet_diagnostic.bc42309.severity=warning +dotnet_diagnostic.bc42322.severity=warning +dotnet_diagnostic.bc42349.severity=warning +dotnet_diagnostic.bc42353.severity=warning +dotnet_diagnostic.bc42354.severity=warning +dotnet_diagnostic.bc42355.severity=warning +dotnet_diagnostic.bc42356.severity=warning +dotnet_diagnostic.bc42358.severity=warning +dotnet_diagnostic.bc42504.severity=warning +dotnet_diagnostic.bc42505.severity=warning +dotnet_diagnostic.cs0067.severity=warning +dotnet_diagnostic.cs0078.severity=warning +dotnet_diagnostic.cs0108.severity=warning +dotnet_diagnostic.cs0109.severity=warning +dotnet_diagnostic.cs0114.severity=warning +dotnet_diagnostic.cs0162.severity=warning +dotnet_diagnostic.cs0164.severity=warning +dotnet_diagnostic.cs0168.severity=warning +dotnet_diagnostic.cs0169.severity=warning +dotnet_diagnostic.cs0183.severity=warning +dotnet_diagnostic.cs0184.severity=warning +dotnet_diagnostic.cs0197.severity=warning +dotnet_diagnostic.cs0219.severity=warning +dotnet_diagnostic.cs0252.severity=warning +dotnet_diagnostic.cs0253.severity=warning +dotnet_diagnostic.cs0414.severity=warning +dotnet_diagnostic.cs0420.severity=warning +dotnet_diagnostic.cs0465.severity=warning +dotnet_diagnostic.cs0469.severity=warning +dotnet_diagnostic.cs0612.severity=warning +dotnet_diagnostic.cs0618.severity=warning +dotnet_diagnostic.cs0628.severity=warning +dotnet_diagnostic.cs0642.severity=warning +dotnet_diagnostic.cs0649.severity=warning +dotnet_diagnostic.cs0652.severity=warning +dotnet_diagnostic.cs0657.severity=warning +dotnet_diagnostic.cs0658.severity=warning +dotnet_diagnostic.cs0659.severity=warning +dotnet_diagnostic.cs0660.severity=warning +dotnet_diagnostic.cs0661.severity=warning +dotnet_diagnostic.cs0665.severity=warning +dotnet_diagnostic.cs0672.severity=warning +dotnet_diagnostic.cs0675.severity=warning +dotnet_diagnostic.cs0693.severity=warning +dotnet_diagnostic.cs1030.severity=warning +dotnet_diagnostic.cs1058.severity=warning +dotnet_diagnostic.cs1066.severity=warning +dotnet_diagnostic.cs1522.severity=warning +dotnet_diagnostic.cs1570.severity=warning +dotnet_diagnostic.cs1571.severity=warning +dotnet_diagnostic.cs1572.severity=warning +dotnet_diagnostic.cs1573.severity=warning +dotnet_diagnostic.cs1574.severity=warning +dotnet_diagnostic.cs1580.severity=warning +dotnet_diagnostic.cs1581.severity=warning +dotnet_diagnostic.cs1584.severity=warning +dotnet_diagnostic.cs1587.severity=warning +dotnet_diagnostic.cs1589.severity=warning +dotnet_diagnostic.cs1590.severity=warning +dotnet_diagnostic.cs1591.severity=warning +dotnet_diagnostic.cs1592.severity=warning +dotnet_diagnostic.cs1710.severity=warning +dotnet_diagnostic.cs1711.severity=warning +dotnet_diagnostic.cs1712.severity=warning +dotnet_diagnostic.cs1717.severity=warning +dotnet_diagnostic.cs1723.severity=warning +dotnet_diagnostic.cs1911.severity=warning +dotnet_diagnostic.cs1957.severity=warning +dotnet_diagnostic.cs1981.severity=warning +dotnet_diagnostic.cs1998.severity=warning +dotnet_diagnostic.cs4014.severity=warning +dotnet_diagnostic.cs7022.severity=warning +dotnet_diagnostic.cs7023.severity=warning +dotnet_diagnostic.cs7095.severity=warning +dotnet_diagnostic.cs8094.severity=warning +dotnet_diagnostic.cs8123.severity=warning +dotnet_diagnostic.cs8321.severity=warning +dotnet_diagnostic.cs8383.severity=warning +dotnet_diagnostic.cs8416.severity=warning +dotnet_diagnostic.cs8417.severity=warning +dotnet_diagnostic.cs8424.severity=warning +dotnet_diagnostic.cs8425.severity=warning +dotnet_diagnostic.cs8509.severity=warning +dotnet_diagnostic.cs8524.severity=warning +dotnet_diagnostic.cs8597.severity=warning +dotnet_diagnostic.cs8600.severity=warning +dotnet_diagnostic.cs8601.severity=warning +dotnet_diagnostic.cs8602.severity=warning +dotnet_diagnostic.cs8603.severity=warning +dotnet_diagnostic.cs8604.severity=warning +dotnet_diagnostic.cs8605.severity=warning +dotnet_diagnostic.cs8607.severity=warning +dotnet_diagnostic.cs8608.severity=warning +dotnet_diagnostic.cs8609.severity=warning +dotnet_diagnostic.cs8610.severity=warning +dotnet_diagnostic.cs8611.severity=warning +dotnet_diagnostic.cs8612.severity=warning +dotnet_diagnostic.cs8613.severity=warning +dotnet_diagnostic.cs8614.severity=warning +dotnet_diagnostic.cs8615.severity=warning +dotnet_diagnostic.cs8616.severity=warning +dotnet_diagnostic.cs8617.severity=warning +dotnet_diagnostic.cs8618.severity=warning +dotnet_diagnostic.cs8619.severity=warning +dotnet_diagnostic.cs8620.severity=warning +dotnet_diagnostic.cs8621.severity=warning +dotnet_diagnostic.cs8622.severity=warning +dotnet_diagnostic.cs8624.severity=warning +dotnet_diagnostic.cs8625.severity=warning +dotnet_diagnostic.cs8629.severity=warning +dotnet_diagnostic.cs8631.severity=warning +dotnet_diagnostic.cs8632.severity=none +dotnet_diagnostic.cs8633.severity=warning +dotnet_diagnostic.cs8634.severity=warning +dotnet_diagnostic.cs8643.severity=warning +dotnet_diagnostic.cs8644.severity=warning +dotnet_diagnostic.cs8645.severity=warning +dotnet_diagnostic.cs8655.severity=warning +dotnet_diagnostic.cs8656.severity=warning +dotnet_diagnostic.cs8667.severity=warning +dotnet_diagnostic.cs8669.severity=none +dotnet_diagnostic.cs8670.severity=warning +dotnet_diagnostic.cs8714.severity=warning +dotnet_diagnostic.cs8762.severity=warning +dotnet_diagnostic.cs8763.severity=warning +dotnet_diagnostic.cs8764.severity=warning +dotnet_diagnostic.cs8765.severity=warning +dotnet_diagnostic.cs8766.severity=warning +dotnet_diagnostic.cs8767.severity=warning +dotnet_diagnostic.cs8768.severity=warning +dotnet_diagnostic.cs8769.severity=warning +dotnet_diagnostic.cs8770.severity=warning +dotnet_diagnostic.cs8774.severity=warning +dotnet_diagnostic.cs8775.severity=warning +dotnet_diagnostic.cs8776.severity=warning +dotnet_diagnostic.cs8777.severity=warning +dotnet_diagnostic.cs8794.severity=warning +dotnet_diagnostic.cs8819.severity=warning +dotnet_diagnostic.cs8824.severity=warning +dotnet_diagnostic.cs8825.severity=warning +dotnet_diagnostic.cs8846.severity=warning +dotnet_diagnostic.cs8847.severity=warning +dotnet_diagnostic.cs8851.severity=warning +dotnet_diagnostic.cs8860.severity=warning +dotnet_diagnostic.cs8892.severity=warning +dotnet_diagnostic.cs8907.severity=warning +dotnet_diagnostic.cs8947.severity=warning +dotnet_diagnostic.cs8960.severity=warning +dotnet_diagnostic.cs8961.severity=warning +dotnet_diagnostic.cs8962.severity=warning +dotnet_diagnostic.cs8963.severity=warning +dotnet_diagnostic.cs8965.severity=warning +dotnet_diagnostic.cs8966.severity=warning +dotnet_diagnostic.cs8971.severity=warning +dotnet_diagnostic.wme006.severity=warning +dotnet_naming_rule.constants_rule.import_to_resharper=as_predefined +dotnet_naming_rule.constants_rule.severity = warning +dotnet_naming_rule.constants_rule.style = upper_camel_case_style +dotnet_naming_rule.constants_rule.symbols=constants_symbols +dotnet_naming_rule.event_rule.import_to_resharper=as_predefined +dotnet_naming_rule.event_rule.severity = warning +dotnet_naming_rule.event_rule.style = upper_camel_case_style +dotnet_naming_rule.event_rule.symbols=event_symbols +dotnet_naming_rule.interfaces_rule.import_to_resharper=as_predefined +dotnet_naming_rule.interfaces_rule.severity = warning +dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style +dotnet_naming_rule.interfaces_rule.symbols=interfaces_symbols +dotnet_naming_rule.locals_rule.import_to_resharper=as_predefined +dotnet_naming_rule.locals_rule.severity = warning +dotnet_naming_rule.locals_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.locals_rule.symbols=locals_symbols +dotnet_naming_rule.local_constants_rule.import_to_resharper=as_predefined +dotnet_naming_rule.local_constants_rule.severity = warning +dotnet_naming_rule.local_constants_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.local_constants_rule.symbols=local_constants_symbols +dotnet_naming_rule.local_functions_rule.import_to_resharper=as_predefined +dotnet_naming_rule.local_functions_rule.severity = warning +dotnet_naming_rule.local_functions_rule.style = upper_camel_case_style +dotnet_naming_rule.local_functions_rule.symbols=local_functions_symbols +dotnet_naming_rule.method_rule.import_to_resharper=as_predefined +dotnet_naming_rule.method_rule.severity = warning +dotnet_naming_rule.method_rule.style = upper_camel_case_style +dotnet_naming_rule.method_rule.symbols=method_symbols +dotnet_naming_rule.parameters_rule.import_to_resharper=as_predefined +dotnet_naming_rule.parameters_rule.severity = warning +dotnet_naming_rule.parameters_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.parameters_rule.symbols=parameters_symbols +dotnet_naming_rule.private_constants_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols=private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols=private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols=private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols=private_static_readonly_symbols +dotnet_naming_rule.property_rule.import_to_resharper=as_predefined +dotnet_naming_rule.property_rule.severity = warning +dotnet_naming_rule.property_rule.style = upper_camel_case_style +dotnet_naming_rule.property_rule.symbols=property_symbols +dotnet_naming_rule.public_fields_rule.import_to_resharper=as_predefined +dotnet_naming_rule.public_fields_rule.severity = warning +dotnet_naming_rule.public_fields_rule.style = upper_camel_case_style +dotnet_naming_rule.public_fields_rule.symbols=public_fields_symbols +dotnet_naming_rule.static_readonly_rule.import_to_resharper=as_predefined +dotnet_naming_rule.static_readonly_rule.severity = warning +dotnet_naming_rule.static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.static_readonly_rule.symbols=static_readonly_symbols +dotnet_naming_rule.types_and_namespaces_rule.import_to_resharper=as_predefined +dotnet_naming_rule.types_and_namespaces_rule.severity = warning +dotnet_naming_rule.types_and_namespaces_rule.style = upper_camel_case_style +dotnet_naming_rule.types_and_namespaces_rule.symbols=types_and_namespaces_symbols +dotnet_naming_rule.type_parameters_rule.import_to_resharper=as_predefined +dotnet_naming_rule.type_parameters_rule.severity = warning +dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style +dotnet_naming_rule.type_parameters_rule.symbols=type_parameters_symbols +dotnet_naming_style.i_upper_camel_case_style.capitalization=pascal_case +dotnet_naming_style.i_upper_camel_case_style.required_prefix=I +dotnet_naming_style.lower_camel_case_style.capitalization=camel_case +dotnet_naming_style.lower_camel_case_style.required_prefix=_ +dotnet_naming_style.lower_camel_case_style_1.capitalization=camel_case +dotnet_naming_style.t_upper_camel_case_style.capitalization=pascal_case +dotnet_naming_style.t_upper_camel_case_style.required_prefix=T +dotnet_naming_style.upper_camel_case_style.capitalization=pascal_case +dotnet_naming_symbols.constants_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.constants_symbols.applicable_kinds=field +dotnet_naming_symbols.constants_symbols.required_modifiers=const +dotnet_naming_symbols.event_symbols.applicable_accessibilities=* +dotnet_naming_symbols.event_symbols.applicable_kinds=event +dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities=* +dotnet_naming_symbols.interfaces_symbols.applicable_kinds=interface +dotnet_naming_symbols.locals_symbols.applicable_accessibilities=* +dotnet_naming_symbols.locals_symbols.applicable_kinds=local +dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities=* +dotnet_naming_symbols.local_constants_symbols.applicable_kinds=local +dotnet_naming_symbols.local_constants_symbols.required_modifiers=const +dotnet_naming_symbols.local_functions_symbols.applicable_accessibilities=* +dotnet_naming_symbols.local_functions_symbols.applicable_kinds=local_function +dotnet_naming_symbols.method_symbols.applicable_accessibilities=* +dotnet_naming_symbols.method_symbols.applicable_kinds=method +dotnet_naming_symbols.parameters_symbols.applicable_accessibilities=* +dotnet_naming_symbols.parameters_symbols.applicable_kinds=parameter +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds=field +dotnet_naming_symbols.private_constants_symbols.required_modifiers=const +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds=field +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds=field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers=static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds=field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers=static,readonly +dotnet_naming_symbols.property_symbols.applicable_accessibilities=* +dotnet_naming_symbols.property_symbols.applicable_kinds=property +dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_fields_symbols.applicable_kinds=field +dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.static_readonly_symbols.applicable_kinds=field +dotnet_naming_symbols.static_readonly_symbols.required_modifiers=static,readonly +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities=* +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds=namespace,class,struct,enum,delegate +dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities=* +dotnet_naming_symbols.type_parameters_symbols.applicable_kinds=type_parameter +dotnet_separate_import_directive_groups=false +dotnet_sort_system_directives_first=true +dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:suggestion +dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:suggestion +dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:suggestion +dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion +dotnet_style_predefined_type_for_member_access=true:suggestion +dotnet_style_qualification_for_event=false:suggestion +dotnet_style_qualification_for_field=false:suggestion +dotnet_style_qualification_for_method=false:suggestion +dotnet_style_qualification_for_property=false:suggestion +dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion +file_header_template= + +# ReSharper properties +resharper_accessor_owner_body=expression_body +resharper_alignment_tab_fill_style=use_spaces +resharper_align_first_arg_by_paren=false +resharper_align_linq_query=false +resharper_align_multiline_argument=true +resharper_align_multiline_array_and_object_initializer=false +resharper_align_multiline_array_initializer=true +resharper_align_multiline_binary_expressions_chain=false +resharper_align_multiline_binary_patterns=false +resharper_align_multiline_ctor_init=true +resharper_align_multiline_expression_braces=false +resharper_align_multiline_implements_list=true +resharper_align_multiline_property_pattern=false +resharper_align_multiline_statement_conditions=true +resharper_align_multiline_switch_expression=false +resharper_align_multiline_type_argument=true +resharper_align_multiline_type_parameter=true +resharper_align_multline_type_parameter_constrains=true +resharper_align_multline_type_parameter_list=false +resharper_align_tuple_components=false +resharper_align_union_type_usage=true +resharper_allow_alias=true +resharper_allow_comment_after_lbrace=false +resharper_allow_far_alignment=false +resharper_always_use_end_of_line_brace_style=false +resharper_apply_auto_detected_rules=false +resharper_apply_on_completion=false +resharper_arguments_anonymous_function=positional +resharper_arguments_literal=positional +resharper_arguments_named=positional +resharper_arguments_other=positional +resharper_arguments_skip_single=false +resharper_arguments_string_literal=positional +resharper_attribute_style=do_not_touch +resharper_autodetect_indent_settings=false +resharper_blank_lines_after_block_statements=1 +resharper_blank_lines_after_case=0 +resharper_blank_lines_after_control_transfer_statements=1 +resharper_blank_lines_after_file_scoped_namespace_directive=1 +resharper_blank_lines_after_imports=1 +resharper_blank_lines_after_multiline_statements=0 +resharper_blank_lines_after_options=1 +resharper_blank_lines_after_start_comment=1 +resharper_blank_lines_after_using_list=1 +resharper_blank_lines_around_accessor=0 +resharper_blank_lines_around_auto_property=1 +resharper_blank_lines_around_block_case_section=0 +resharper_blank_lines_around_class_definition=1 +resharper_blank_lines_around_field=1 +resharper_blank_lines_around_function_declaration=0 +resharper_blank_lines_around_function_definition=1 +resharper_blank_lines_around_global_attribute=0 +resharper_blank_lines_around_invocable=1 +resharper_blank_lines_around_local_method=1 +resharper_blank_lines_around_multiline_case_section=0 +resharper_blank_lines_around_namespace=1 +resharper_blank_lines_around_other_declaration=0 +resharper_blank_lines_around_property=1 +resharper_blank_lines_around_razor_functions=1 +resharper_blank_lines_around_razor_helpers=1 +resharper_blank_lines_around_razor_sections=1 +resharper_blank_lines_around_region=1 +resharper_blank_lines_around_single_line_accessor=0 +resharper_blank_lines_around_single_line_auto_property=0 +resharper_blank_lines_around_single_line_field=0 +resharper_blank_lines_around_single_line_function_definition=0 +resharper_blank_lines_around_single_line_invocable=0 +resharper_blank_lines_around_single_line_local_method=0 +resharper_blank_lines_around_single_line_property=0 +resharper_blank_lines_around_single_line_type=0 +resharper_blank_lines_around_type=1 +resharper_blank_lines_before_block_statements=0 +resharper_blank_lines_before_case=0 +resharper_blank_lines_before_control_transfer_statements=0 +resharper_blank_lines_before_multiline_statements=0 +resharper_blank_lines_before_single_line_comment=0 +resharper_blank_lines_inside_namespace=0 +resharper_blank_lines_inside_region=1 +resharper_blank_lines_inside_type=0 +resharper_blank_line_after_pi=true +resharper_braces_for_dowhile=required +resharper_braces_for_fixed=required +resharper_braces_for_for=required_for_multiline +resharper_braces_for_foreach=required_for_multiline +resharper_braces_for_ifelse=not_required_for_both +resharper_braces_for_lock=required +resharper_braces_for_using=required +resharper_braces_for_while=required_for_multiline +resharper_braces_redundant=true +resharper_break_template_declaration=line_break +resharper_can_use_global_alias=true +resharper_configure_await_analysis_mode=disabled +resharper_constructor_or_destructor_body=expression_body +resharper_continuous_indent_multiplier=1 +resharper_continuous_line_indent=single +resharper_cpp_align_multiline_argument=true +resharper_cpp_align_multiline_calls_chain=true +resharper_cpp_align_multiline_extends_list=true +resharper_cpp_align_multiline_for_stmt=true +resharper_cpp_align_multiline_parameter=true +resharper_cpp_align_multiple_declaration=true +resharper_cpp_align_ternary=align_not_nested +resharper_cpp_anonymous_method_declaration_braces=next_line +resharper_cpp_case_block_braces=next_line_shifted_2 +resharper_cpp_empty_block_style=multiline +resharper_cpp_indent_switch_labels=false +resharper_cpp_insert_final_newline=false +resharper_cpp_int_align_comments=false +resharper_cpp_invocable_declaration_braces=next_line +resharper_cpp_max_line_length=120 +resharper_cpp_new_line_before_catch=true +resharper_cpp_new_line_before_else=true +resharper_cpp_new_line_before_while=true +resharper_cpp_other_braces=next_line +resharper_cpp_space_around_binary_operator=true +resharper_cpp_type_declaration_braces=next_line +resharper_cpp_wrap_arguments_style=wrap_if_long +resharper_cpp_wrap_lines=true +resharper_cpp_wrap_parameters_style=wrap_if_long +resharper_csharp_align_multiline_argument=false +resharper_csharp_align_multiline_calls_chain=false +resharper_csharp_align_multiline_expression=false +resharper_csharp_align_multiline_extends_list=false +resharper_csharp_align_multiline_for_stmt=false +resharper_csharp_align_multiline_parameter=false +resharper_csharp_align_multiple_declaration=true +resharper_csharp_empty_block_style=together +resharper_csharp_insert_final_newline=true +resharper_csharp_int_align_comments=true +resharper_csharp_max_line_length=144 +resharper_csharp_naming_rule.enum_member=AaBb +resharper_csharp_naming_rule.method_property_event=AaBb +resharper_csharp_naming_rule.other=AaBb +resharper_csharp_new_line_before_while=false +resharper_csharp_prefer_qualified_reference=false +resharper_csharp_space_after_unary_operator=false +resharper_csharp_wrap_arguments_style=wrap_if_long +resharper_csharp_wrap_before_binary_opsign=true +resharper_csharp_wrap_for_stmt_header_style=wrap_if_long +resharper_csharp_wrap_lines=true +resharper_csharp_wrap_parameters_style=wrap_if_long +resharper_css_brace_style=end_of_line +resharper_css_insert_final_newline=false +resharper_css_keep_blank_lines_between_declarations=1 +resharper_css_max_line_length=120 +resharper_css_wrap_lines=true +resharper_cxxcli_property_declaration_braces=next_line +resharper_declarations_style=separate_lines +resharper_default_exception_variable_name=e +resharper_default_value_when_type_evident=default_literal +resharper_default_value_when_type_not_evident=default_literal +resharper_delete_quotes_from_solid_values=false +resharper_disable_blank_line_changes=false +resharper_disable_formatter=false +resharper_disable_indenter=false +resharper_disable_int_align=false +resharper_disable_line_break_changes=false +resharper_disable_line_break_removal=false +resharper_disable_space_changes=false +resharper_disable_space_changes_before_trailing_comment=false +resharper_dont_remove_extra_blank_lines=false +resharper_enable_wrapping=false +resharper_enforce_line_ending_style=false +resharper_event_handler_pattern_long=$object$On$event$ +resharper_event_handler_pattern_short=On$event$ +resharper_expression_braces=inside +resharper_expression_pars=inside +resharper_extra_spaces=remove_all +resharper_force_attribute_style=separate +resharper_force_chop_compound_do_expression=false +resharper_force_chop_compound_if_expression=false +resharper_force_chop_compound_while_expression=false +resharper_force_control_statements_braces=do_not_change +resharper_force_linebreaks_inside_complex_literals=true +resharper_force_variable_declarations_on_new_line=false +resharper_format_leading_spaces_decl=false +resharper_free_block_braces=next_line +resharper_function_declaration_return_type_style=do_not_change +resharper_function_definition_return_type_style=do_not_change +resharper_generator_mode=false +resharper_html_attribute_indent=align_by_first_attribute +resharper_html_insert_final_newline=false +resharper_html_linebreak_before_elements=body,div,p,form,h1,h2,h3 +resharper_html_max_blank_lines_between_tags=2 +resharper_html_max_line_length=120 +resharper_html_pi_attribute_style=on_single_line +resharper_html_space_before_self_closing=false +resharper_html_wrap_lines=true +resharper_ignore_space_preservation=false +resharper_include_prefix_comment_in_indent=false +resharper_indent_access_specifiers_from_class=false +resharper_indent_aligned_ternary=true +resharper_indent_anonymous_method_block=false +resharper_indent_braces_inside_statement_conditions=true +resharper_indent_case_from_select=true +resharper_indent_child_elements=OneIndent +resharper_indent_class_members_from_access_specifiers=false +resharper_indent_comment=true +resharper_indent_inside_namespace=true +resharper_indent_invocation_pars=inside +resharper_indent_left_par_inside_expression=false +resharper_indent_method_decl_pars=inside +resharper_indent_nested_fixed_stmt=false +resharper_indent_nested_foreach_stmt=true +resharper_indent_nested_for_stmt=true +resharper_indent_nested_lock_stmt=false +resharper_indent_nested_usings_stmt=false +resharper_indent_nested_while_stmt=true +resharper_indent_pars=inside +resharper_indent_preprocessor_directives=none +resharper_indent_preprocessor_if=no_indent +resharper_indent_preprocessor_other=no_indent +resharper_indent_preprocessor_region=usual_indent +resharper_indent_statement_pars=inside +resharper_indent_text=OneIndent +resharper_indent_typearg_angles=inside +resharper_indent_typeparam_angles=inside +resharper_indent_type_constraints=true +resharper_indent_wrapped_function_names=false +resharper_instance_members_qualify_declared_in=this_class, base_class +resharper_int_align=true +resharper_int_align_assignments=true +resharper_int_align_binary_expressions=false +resharper_int_align_declaration_names=false +resharper_int_align_eq=false +resharper_int_align_fields=true +resharper_int_align_fix_in_adjacent=true +resharper_int_align_invocations=true +resharper_int_align_methods=true +resharper_int_align_nested_ternary=true +resharper_int_align_parameters=false +resharper_int_align_properties=true +resharper_int_align_property_patterns=true +resharper_int_align_switch_expressions=true +resharper_int_align_switch_sections=true +resharper_int_align_variables=true +resharper_js_align_multiline_parameter=false +resharper_js_align_multiple_declaration=false +resharper_js_align_ternary=none +resharper_js_brace_style=end_of_line +resharper_js_empty_block_style=multiline +resharper_js_indent_switch_labels=false +resharper_js_insert_final_newline=false +resharper_js_keep_blank_lines_between_declarations=2 +resharper_js_max_line_length=120 +resharper_js_new_line_before_catch=false +resharper_js_new_line_before_else=false +resharper_js_new_line_before_finally=false +resharper_js_new_line_before_while=false +resharper_js_space_around_binary_operator=true +resharper_js_wrap_arguments_style=chop_if_long +resharper_js_wrap_before_binary_opsign=false +resharper_js_wrap_for_stmt_header_style=chop_if_long +resharper_js_wrap_lines=true +resharper_js_wrap_parameters_style=chop_if_long +resharper_keep_blank_lines_in_code=2 +resharper_keep_blank_lines_in_declarations=2 +resharper_keep_existing_attribute_arrangement=false +resharper_keep_existing_declaration_block_arrangement=false +resharper_keep_existing_declaration_parens_arrangement=true +resharper_keep_existing_embedded_arrangement=false +resharper_keep_existing_embedded_block_arrangement=false +resharper_keep_existing_enum_arrangement=false +resharper_keep_existing_expr_member_arrangement=false +resharper_keep_existing_initializer_arrangement=false +resharper_keep_existing_invocation_parens_arrangement=true +resharper_keep_existing_property_patterns_arrangement=true +resharper_keep_existing_switch_expression_arrangement=false +resharper_keep_nontrivial_alias=true +resharper_keep_user_linebreaks=true +resharper_keep_user_wrapping=true +resharper_linebreaks_around_razor_statements=true +resharper_linebreaks_inside_tags_for_elements_longer_than=2147483647 +resharper_linebreaks_inside_tags_for_elements_with_child_elements=true +resharper_linebreaks_inside_tags_for_multiline_elements=true +resharper_linebreak_before_all_elements=false +resharper_linebreak_before_multiline_elements=true +resharper_linebreak_before_singleline_elements=false +resharper_line_break_after_colon_in_member_initializer_lists=do_not_change +resharper_line_break_after_comma_in_member_initializer_lists=false +resharper_line_break_before_comma_in_member_initializer_lists=false +resharper_line_break_before_requires_clause=do_not_change +resharper_linkage_specification_braces=end_of_line +resharper_linkage_specification_indentation=none +resharper_local_function_body=expression_body +resharper_macro_block_begin= +resharper_macro_block_end= +resharper_max_array_initializer_elements_on_line=10000 +resharper_max_attribute_length_for_same_line=38 +resharper_max_enum_members_on_line=1 +resharper_max_formal_parameters_on_line=10000 +resharper_max_initializer_elements_on_line=1 +resharper_max_invocation_arguments_on_line=10000 +resharper_media_query_style=same_line +resharper_member_initializer_list_style=do_not_change +resharper_method_or_operator_body=expression_body +resharper_min_blank_lines_after_imports=0 +resharper_min_blank_lines_around_fields=0 +resharper_min_blank_lines_around_functions=1 +resharper_min_blank_lines_around_types=1 +resharper_min_blank_lines_between_declarations=1 +resharper_namespace_declaration_braces=next_line +resharper_namespace_indentation=all +resharper_nested_ternary_style=autodetect +resharper_new_line_before_enumerators=true +resharper_normalize_tag_names=false +resharper_no_indent_inside_elements=html,body,thead,tbody,tfoot +resharper_no_indent_inside_if_element_longer_than=200 +resharper_object_creation_when_type_evident=target_typed +resharper_object_creation_when_type_not_evident=explicitly_typed +resharper_old_engine=false +resharper_options_braces_pointy=false +resharper_outdent_binary_ops=true +resharper_outdent_binary_pattern_ops=false +resharper_outdent_commas=false +resharper_outdent_dots=false +resharper_outdent_namespace_member=false +resharper_outdent_statement_labels=false +resharper_outdent_ternary_ops=false +resharper_parentheses_non_obvious_operations=none, bitwise, bitwise_inclusive_or, bitwise_exclusive_or, shift, bitwise_and +resharper_parentheses_redundancy_style=remove_if_not_clarifies_precedence +resharper_parentheses_same_type_operations=false +resharper_pi_attributes_indent=align_by_first_attribute +resharper_place_attribute_on_same_line=false +resharper_place_class_decorator_on_the_same_line=false +resharper_place_comments_at_first_column=false +resharper_place_constructor_initializer_on_same_line=false +resharper_place_each_decorator_on_new_line=false +resharper_place_event_attribute_on_same_line=false +resharper_place_expr_accessor_on_single_line=true +resharper_place_expr_method_on_single_line=false +resharper_place_expr_property_on_single_line=false +resharper_place_field_decorator_on_the_same_line=false +resharper_place_linq_into_on_new_line=true +resharper_place_method_decorator_on_the_same_line=false +resharper_place_namespace_definitions_on_same_line=false +resharper_place_property_attribute_on_same_line=false +resharper_place_property_decorator_on_the_same_line=false +resharper_place_simple_case_statement_on_same_line=if_owner_is_single_line +resharper_place_simple_embedded_statement_on_same_line=false +resharper_place_simple_enum_on_single_line=true +resharper_place_simple_initializer_on_single_line=true +resharper_place_simple_property_pattern_on_single_line=true +resharper_place_simple_switch_expression_on_single_line=true +resharper_place_template_args_on_new_line=false +resharper_place_type_constraints_on_same_line=true +resharper_prefer_explicit_discard_declaration=false +resharper_prefer_separate_deconstructed_variables_declaration=false +resharper_preserve_spaces_inside_tags=pre,textarea +resharper_properties_style=separate_lines_for_nonsingle +resharper_protobuf_brace_style=end_of_line +resharper_protobuf_empty_block_style=together_same_line +resharper_protobuf_insert_final_newline=false +resharper_protobuf_max_line_length=120 +resharper_protobuf_wrap_lines=true +resharper_qualified_using_at_nested_scope=false +resharper_quote_style=doublequoted +resharper_razor_prefer_qualified_reference=true +resharper_remove_blank_lines_near_braces=false +resharper_remove_blank_lines_near_braces_in_code=true +resharper_remove_blank_lines_near_braces_in_declarations=true +resharper_remove_this_qualifier=true +resharper_requires_expression_braces=next_line +resharper_resx_attribute_indent=single_indent +resharper_resx_insert_final_newline=false +resharper_resx_linebreak_before_elements= +resharper_resx_max_blank_lines_between_tags=0 +resharper_resx_max_line_length=2147483647 +resharper_resx_pi_attribute_style=do_not_touch +resharper_resx_space_before_self_closing=false +resharper_resx_wrap_lines=false +resharper_resx_wrap_tags_and_pi=false +resharper_resx_wrap_text=false +resharper_selector_style=same_line +resharper_show_autodetect_configure_formatting_tip=true +resharper_simple_blocks=do_not_change +resharper_simple_block_style=do_not_change +resharper_simple_case_statement_style=do_not_change +resharper_simple_embedded_statement_style=do_not_change +resharper_single_statement_function_style=do_not_change +resharper_sort_attributes=false +resharper_sort_class_selectors=false +resharper_sort_usings=true +resharper_sort_usings_lowercase_first=false +resharper_spaces_around_eq_in_attribute=false +resharper_spaces_around_eq_in_pi_attribute=false +resharper_spaces_inside_tags=false +resharper_space_after_arrow=true +resharper_space_after_attributes=true +resharper_space_after_attribute_target_colon=true +resharper_space_after_cast=false +resharper_space_after_colon=true +resharper_space_after_colon_in_case=true +resharper_space_after_colon_in_inheritance_clause=true +resharper_space_after_colon_in_type_annotation=true +resharper_space_after_comma=true +resharper_space_after_for_colon=true +resharper_space_after_function_comma=true +resharper_space_after_keywords_in_control_flow_statements=true +resharper_space_after_last_attribute=false +resharper_space_after_last_pi_attribute=false +resharper_space_after_media_colon=true +resharper_space_after_media_comma=true +resharper_space_after_operator_keyword=true +resharper_space_after_property_colon=true +resharper_space_after_property_semicolon=true +resharper_space_after_ptr_in_data_member=true +resharper_space_after_ptr_in_data_members=false +resharper_space_after_ptr_in_method=true +resharper_space_after_ref_in_data_member=true +resharper_space_after_ref_in_data_members=false +resharper_space_after_ref_in_method=true +resharper_space_after_selector_comma=true +resharper_space_after_semicolon_in_for_statement=true +resharper_space_after_separator=false +resharper_space_after_ternary_colon=true +resharper_space_after_ternary_quest=true +resharper_space_after_triple_slash=true +resharper_space_after_type_parameter_constraint_colon=true +resharper_space_around_additive_op=true +resharper_space_around_alias_eq=true +resharper_space_around_assignment_op=true +resharper_space_around_assignment_operator=true +resharper_space_around_attribute_match_operator=false +resharper_space_around_deref_in_trailing_return_type=true +resharper_space_around_lambda_arrow=true +resharper_space_around_member_access_operator=false +resharper_space_around_operator=true +resharper_space_around_pipe_or_amper_in_type_usage=true +resharper_space_around_relational_op=true +resharper_space_around_selector_operator=true +resharper_space_around_shift_op=true +resharper_space_around_stmt_colon=true +resharper_space_around_ternary_operator=true +resharper_space_before_array_rank_parentheses=false +resharper_space_before_arrow=true +resharper_space_before_attribute_target_colon=false +resharper_space_before_checked_parentheses=false +resharper_space_before_colon=false +resharper_space_before_colon_in_case=false +resharper_space_before_colon_in_inheritance_clause=true +resharper_space_before_colon_in_type_annotation=false +resharper_space_before_comma=false +resharper_space_before_default_parentheses=false +resharper_space_before_empty_invocation_parentheses=false +resharper_space_before_empty_method_parentheses=false +resharper_space_before_for_colon=true +resharper_space_before_function_comma=false +resharper_space_before_initializer_braces=false +resharper_space_before_invocation_parentheses=false +resharper_space_before_label_colon=false +resharper_space_before_lambda_parentheses=false +resharper_space_before_media_colon=false +resharper_space_before_media_comma=false +resharper_space_before_method_parentheses=false +resharper_space_before_nameof_parentheses=false +resharper_space_before_new_parentheses=false +resharper_space_before_nullable_mark=false +resharper_space_before_open_square_brackets=false +resharper_space_before_pointer_asterik_declaration=false +resharper_space_before_property_colon=false +resharper_space_before_property_semicolon=false +resharper_space_before_ptr_in_abstract_decl=false +resharper_space_before_ptr_in_data_member=false +resharper_space_before_ptr_in_data_members=true +resharper_space_before_ptr_in_method=false +resharper_space_before_ref_in_abstract_decl=false +resharper_space_before_ref_in_data_member=false +resharper_space_before_ref_in_data_members=true +resharper_space_before_ref_in_method=false +resharper_space_before_selector_comma=false +resharper_space_before_semicolon=false +resharper_space_before_semicolon_in_for_statement=false +resharper_space_before_separator=false +resharper_space_before_singleline_accessorholder=true +resharper_space_before_sizeof_parentheses=false +resharper_space_before_template_args=false +resharper_space_before_template_params=true +resharper_space_before_ternary_colon=true +resharper_space_before_ternary_quest=true +resharper_space_before_trailing_comment=true +resharper_space_before_typeof_parentheses=false +resharper_space_before_type_argument_angle=false +resharper_space_before_type_parameters_brackets=false +resharper_space_before_type_parameter_angle=false +resharper_space_before_type_parameter_constraint_colon=true +resharper_space_before_type_parameter_parentheses=true +resharper_space_between_accessors_in_singleline_property=true +resharper_space_between_attribute_sections=true +resharper_space_between_closing_angle_brackets_in_template_args=false +resharper_space_between_empty_square_brackets=false +resharper_space_between_keyword_and_expression=true +resharper_space_between_keyword_and_type=true +resharper_space_between_method_call_empty_parameter_list_parentheses=false +resharper_space_between_method_call_name_and_opening_parenthesis=false +resharper_space_between_method_call_parameter_list_parentheses=false +resharper_space_between_method_declaration_empty_parameter_list_parentheses=false +resharper_space_between_method_declaration_name_and_open_parenthesis=false +resharper_space_between_method_declaration_parameter_list_parentheses=false +resharper_space_between_parentheses_of_control_flow_statements=false +resharper_space_between_square_brackets=false +resharper_space_between_typecast_parentheses=false +resharper_space_colon_after=true +resharper_space_colon_before=false +resharper_space_comma=true +resharper_space_equals=true +resharper_space_inside_braces=true +resharper_space_in_singleline_accessorholder=true +resharper_space_in_singleline_anonymous_method=true +resharper_space_in_singleline_method=true +resharper_space_near_postfix_and_prefix_op=false +resharper_space_within_array_initialization_braces=false +resharper_space_within_array_rank_empty_parentheses=false +resharper_space_within_array_rank_parentheses=false +resharper_space_within_attribute_angles=false +resharper_space_within_attribute_match_brackets=false +resharper_space_within_checked_parentheses=false +resharper_space_within_default_parentheses=false +resharper_space_within_empty_braces=true +resharper_space_within_empty_initializer_braces=false +resharper_space_within_empty_invocation_parentheses=false +resharper_space_within_empty_method_parentheses=false +resharper_space_within_empty_object_literal_braces=false +resharper_space_within_empty_template_params=false +resharper_space_within_expression_parentheses=false +resharper_space_within_function_parentheses=false +resharper_space_within_import_braces=true +resharper_space_within_initializer_braces=false +resharper_space_within_invocation_parentheses=false +resharper_space_within_media_block=true +resharper_space_within_media_parentheses=false +resharper_space_within_method_parentheses=false +resharper_space_within_nameof_parentheses=false +resharper_space_within_new_parentheses=false +resharper_space_within_object_literal_braces=true +resharper_space_within_parentheses=false +resharper_space_within_property_block=true +resharper_space_within_single_line_array_initializer_braces=true +resharper_space_within_sizeof_parentheses=false +resharper_space_within_template_args=false +resharper_space_within_template_argument=false +resharper_space_within_template_params=false +resharper_space_within_tuple_parentheses=false +resharper_space_within_typeof_parentheses=false +resharper_space_within_type_argument_angles=false +resharper_space_within_type_parameters_brackets=false +resharper_space_within_type_parameter_angles=false +resharper_space_within_type_parameter_parentheses=false +resharper_special_else_if_treatment=true +resharper_static_members_qualify_members=none +resharper_static_members_qualify_with=declared_type +resharper_stick_comment=true +resharper_support_vs_event_naming_pattern=true +resharper_termination_style=ensure_semicolon +resharper_toplevel_function_declaration_return_type_style=do_not_change +resharper_toplevel_function_definition_return_type_style=do_not_change +resharper_trailing_comma_in_multiline_lists=true +resharper_trailing_comma_in_singleline_lists=false +resharper_types_braces=end_of_line +resharper_use_continuous_indent_inside_initializer_braces=true +resharper_use_continuous_indent_inside_parens=true +resharper_use_continuous_line_indent_in_expression_braces=false +resharper_use_continuous_line_indent_in_method_pars=false +resharper_use_heuristics_for_body_style=true +resharper_use_indents_from_main_language_in_file=true +resharper_use_indent_from_previous_element=true +resharper_use_indent_from_vs=false +resharper_use_roslyn_logic_for_evident_types=false +resharper_vb_align_multiline_argument=true +resharper_vb_align_multiline_expression=true +resharper_vb_align_multiline_parameter=true +resharper_vb_align_multiple_declaration=true +resharper_vb_insert_final_newline=false +resharper_vb_max_line_length=120 +resharper_vb_place_field_attribute_on_same_line=true +resharper_vb_place_method_attribute_on_same_line=false +resharper_vb_place_type_attribute_on_same_line=false +resharper_vb_prefer_qualified_reference=false +resharper_vb_space_after_unary_operator=true +resharper_vb_space_around_multiplicative_op=false +resharper_vb_wrap_arguments_style=wrap_if_long +resharper_vb_wrap_before_binary_opsign=false +resharper_vb_wrap_lines=true +resharper_vb_wrap_parameters_style=wrap_if_long +resharper_wrap_after_binary_opsign=true +resharper_wrap_after_declaration_lpar=false +resharper_wrap_after_dot=false +resharper_wrap_after_dot_in_method_calls=false +resharper_wrap_after_expression_lbrace=true +resharper_wrap_after_invocation_lpar=false +resharper_wrap_around_elements=true +resharper_wrap_array_initializer_style=chop_always +resharper_wrap_array_literals=chop_if_long +resharper_wrap_base_clause_style=wrap_if_long +resharper_wrap_before_arrow_with_expressions=true +resharper_wrap_before_binary_pattern_op=true +resharper_wrap_before_colon=false +resharper_wrap_before_comma=false +resharper_wrap_before_comma_in_base_clause=false +resharper_wrap_before_declaration_lpar=false +resharper_wrap_before_declaration_rpar=false +resharper_wrap_before_dot=true +resharper_wrap_before_eq=false +resharper_wrap_before_expression_rbrace=true +resharper_wrap_before_extends_colon=false +resharper_wrap_before_first_type_parameter_constraint=false +resharper_wrap_before_invocation_lpar=false +resharper_wrap_before_invocation_rpar=false +resharper_wrap_before_linq_expression=false +resharper_wrap_before_ternary_opsigns=true +resharper_wrap_before_type_parameter_langle=false +resharper_wrap_braced_init_list_style=wrap_if_long +resharper_wrap_chained_binary_expressions=chop_if_long +resharper_wrap_chained_binary_patterns=wrap_if_long +resharper_wrap_chained_method_calls=wrap_if_long +resharper_wrap_ctor_initializer_style=wrap_if_long +resharper_wrap_enumeration_style=chop_if_long +resharper_wrap_enum_declaration=chop_always +resharper_wrap_enum_style=do_not_change +resharper_wrap_extends_list_style=wrap_if_long +resharper_wrap_imports=chop_if_long +resharper_wrap_multiple_declaration_style=chop_if_long +resharper_wrap_multiple_type_parameter_constraints_style=chop_if_long +resharper_wrap_object_literals=chop_if_long +resharper_wrap_property_pattern=chop_if_long +resharper_wrap_switch_expression=chop_always +resharper_wrap_ternary_expr_style=chop_if_long +resharper_wrap_union_type_usage=chop_if_long +resharper_wrap_verbatim_interpolated_strings=no_wrap +resharper_xmldoc_attribute_indent=single_indent +resharper_xmldoc_insert_final_newline=false +resharper_xmldoc_linebreak_before_elements=summary,remarks,example,returns,param,typeparam,value,para +resharper_xmldoc_max_blank_lines_between_tags=0 +resharper_xmldoc_max_line_length=120 +resharper_xmldoc_pi_attribute_style=do_not_touch +resharper_xmldoc_space_before_self_closing=true +resharper_xmldoc_wrap_lines=true +resharper_xmldoc_wrap_tags_and_pi=true +resharper_xmldoc_wrap_text=true +resharper_xml_attribute_indent=align_by_first_attribute +resharper_xml_insert_final_newline=false +resharper_xml_linebreak_before_elements= +resharper_xml_max_blank_lines_between_tags=2 +resharper_xml_max_line_length=120 +resharper_xml_pi_attribute_style=do_not_touch +resharper_xml_space_before_self_closing=true +resharper_xml_wrap_lines=true +resharper_xml_wrap_tags_and_pi=true +resharper_xml_wrap_text=false + +# ReSharper inspection severities +resharper_abstract_class_constructor_can_be_made_protected_highlighting=hint +resharper_access_rights_in_text_highlighting=warning +resharper_access_to_disposed_closure_highlighting=warning +resharper_access_to_for_each_variable_in_closure_highlighting=warning +resharper_access_to_modified_closure_highlighting=warning +resharper_access_to_static_member_via_derived_type_highlighting=warning +resharper_address_of_marshal_by_ref_object_highlighting=warning +resharper_amd_dependency_path_problem_highlighting=none +resharper_amd_external_module_highlighting=suggestion +resharper_angular_html_banana_highlighting=warning +resharper_annotate_can_be_null_parameter_highlighting=none +resharper_annotate_can_be_null_type_member_highlighting=none +resharper_annotate_not_null_parameter_highlighting=none +resharper_annotate_not_null_type_member_highlighting=none +resharper_annotation_conflict_in_hierarchy_highlighting=warning +resharper_annotation_redundancy_at_value_type_highlighting=warning +resharper_annotation_redundancy_in_hierarchy_highlighting=warning +resharper_arguments_style_anonymous_function_highlighting=hint +resharper_arguments_style_literal_highlighting=hint +resharper_arguments_style_named_expression_highlighting=hint +resharper_arguments_style_other_highlighting=hint +resharper_arguments_style_string_literal_highlighting=hint +resharper_arrange_accessor_owner_body_highlighting=suggestion +resharper_arrange_attributes_highlighting=none +resharper_arrange_constructor_or_destructor_body_highlighting=hint +resharper_arrange_default_value_when_type_evident_highlighting=suggestion +resharper_arrange_default_value_when_type_not_evident_highlighting=hint +resharper_arrange_local_function_body_highlighting=hint +resharper_arrange_method_or_operator_body_highlighting=hint +resharper_arrange_missing_parentheses_highlighting=hint +resharper_arrange_namespace_body_highlighting=hint +resharper_arrange_object_creation_when_type_evident_highlighting=suggestion +resharper_arrange_object_creation_when_type_not_evident_highlighting=hint +resharper_arrange_redundant_parentheses_highlighting=hint +resharper_arrange_static_member_qualifier_highlighting=hint +resharper_arrange_this_qualifier_highlighting=hint +resharper_arrange_trailing_comma_in_multiline_lists_highlighting=hint +resharper_arrange_trailing_comma_in_singleline_lists_highlighting=hint +resharper_arrange_type_member_modifiers_highlighting=hint +resharper_arrange_type_modifiers_highlighting=hint +resharper_arrange_var_keywords_in_deconstructing_declaration_highlighting=suggestion +resharper_asp_content_placeholder_not_resolved_highlighting=error +resharper_asp_custom_page_parser_filter_type_highlighting=warning +resharper_asp_dead_code_highlighting=warning +resharper_asp_entity_highlighting=warning +resharper_asp_image_highlighting=warning +resharper_asp_invalid_control_type_highlighting=error +resharper_asp_not_resolved_highlighting=error +resharper_asp_ods_method_reference_resolve_error_highlighting=error +resharper_asp_resolve_warning_highlighting=warning +resharper_asp_skin_not_resolved_highlighting=error +resharper_asp_tag_attribute_with_optional_value_highlighting=warning +resharper_asp_theme_not_resolved_highlighting=error +resharper_asp_unused_register_directive_highlighting_highlighting=warning +resharper_asp_warning_highlighting=warning +resharper_assigned_value_is_never_used_highlighting=warning +resharper_assigned_value_wont_be_assigned_to_corresponding_field_highlighting=warning +resharper_assignment_in_conditional_expression_highlighting=warning +resharper_assignment_in_condition_expression_highlighting=warning +resharper_assignment_is_fully_discarded_highlighting=warning +resharper_assign_null_to_not_null_attribute_highlighting=warning +resharper_assign_to_constant_highlighting=error +resharper_assign_to_implicit_global_in_function_scope_highlighting=warning +resharper_asxx_path_error_highlighting=warning +resharper_async_iterator_invocation_without_await_foreach_highlighting=warning +resharper_async_void_lambda_highlighting=warning +resharper_async_void_method_highlighting=none +resharper_auto_property_can_be_made_get_only_global_highlighting=suggestion +resharper_auto_property_can_be_made_get_only_local_highlighting=suggestion +resharper_bad_attribute_brackets_spaces_highlighting=none +resharper_bad_braces_spaces_highlighting=none +resharper_bad_child_statement_indent_highlighting=warning +resharper_bad_colon_spaces_highlighting=none +resharper_bad_comma_spaces_highlighting=none +resharper_bad_control_braces_indent_highlighting=suggestion +resharper_bad_control_braces_line_breaks_highlighting=none +resharper_bad_declaration_braces_indent_highlighting=none +resharper_bad_declaration_braces_line_breaks_highlighting=none +resharper_bad_empty_braces_line_breaks_highlighting=none +resharper_bad_expression_braces_indent_highlighting=none +resharper_bad_expression_braces_line_breaks_highlighting=none +resharper_bad_generic_brackets_spaces_highlighting=none +resharper_bad_indent_highlighting=none +resharper_bad_linq_line_breaks_highlighting=none +resharper_bad_list_line_breaks_highlighting=none +resharper_bad_member_access_spaces_highlighting=none +resharper_bad_namespace_braces_indent_highlighting=none +resharper_bad_parens_line_breaks_highlighting=none +resharper_bad_parens_spaces_highlighting=none +resharper_bad_preprocessor_indent_highlighting=none +resharper_bad_semicolon_spaces_highlighting=none +resharper_bad_spaces_after_keyword_highlighting=none +resharper_bad_square_brackets_spaces_highlighting=none +resharper_bad_switch_braces_indent_highlighting=none +resharper_bad_symbol_spaces_highlighting=none +resharper_base_member_has_params_highlighting=warning +resharper_base_method_call_with_default_parameter_highlighting=warning +resharper_base_object_equals_is_object_equals_highlighting=warning +resharper_base_object_get_hash_code_call_in_get_hash_code_highlighting=warning +resharper_bitwise_operator_on_enum_without_flags_highlighting=warning +resharper_block_scope_redeclaration_highlighting=error +resharper_built_in_type_reference_style_for_member_access_highlighting=hint +resharper_built_in_type_reference_style_highlighting=hint +resharper_by_ref_argument_is_volatile_field_highlighting=warning +resharper_caller_callee_using_error_highlighting=error +resharper_caller_callee_using_highlighting=warning +resharper_cannot_apply_equality_operator_to_type_highlighting=warning +resharper_center_tag_is_obsolete_highlighting=warning +resharper_check_for_reference_equality_instead_1_highlighting=suggestion +resharper_check_for_reference_equality_instead_2_highlighting=suggestion +resharper_check_for_reference_equality_instead_3_highlighting=suggestion +resharper_check_for_reference_equality_instead_4_highlighting=suggestion +resharper_check_namespace_highlighting=warning +resharper_class_cannot_be_instantiated_highlighting=warning +resharper_class_can_be_sealed_global_highlighting=none +resharper_class_can_be_sealed_local_highlighting=none +resharper_class_highlighting=suggestion +resharper_class_never_instantiated_global_highlighting=suggestion +resharper_class_never_instantiated_local_highlighting=suggestion +resharper_class_with_virtual_members_never_inherited_global_highlighting=suggestion +resharper_class_with_virtual_members_never_inherited_local_highlighting=suggestion +resharper_clear_attribute_is_obsolete_all_highlighting=warning +resharper_clear_attribute_is_obsolete_highlighting=warning +resharper_closure_on_modified_variable_highlighting=warning +resharper_coerced_equals_using_highlighting=warning +resharper_coerced_equals_using_with_null_undefined_highlighting=none +resharper_collection_never_queried_global_highlighting=warning +resharper_collection_never_queried_local_highlighting=warning +resharper_collection_never_updated_global_highlighting=warning +resharper_collection_never_updated_local_highlighting=warning +resharper_comma_not_valid_here_highlighting=error +resharper_comment_typo_highlighting=suggestion +resharper_common_js_external_module_highlighting=suggestion +resharper_compare_non_constrained_generic_with_null_highlighting=none +resharper_compare_of_floats_by_equality_operator_highlighting=none +resharper_conditional_ternary_equal_branch_highlighting=warning +resharper_condition_is_always_const_highlighting=warning +resharper_condition_is_always_true_or_false_highlighting=warning +resharper_confusing_char_as_integer_in_constructor_highlighting=warning +resharper_constant_conditional_access_qualifier_highlighting=warning +resharper_constant_null_coalescing_condition_highlighting=warning +resharper_constructor_call_not_used_highlighting=warning +resharper_constructor_initializer_loop_highlighting=warning +resharper_container_annotation_redundancy_highlighting=warning +resharper_context_value_is_provided_highlighting=none +resharper_contract_annotation_not_parsed_highlighting=warning +resharper_convert_closure_to_method_group_highlighting=suggestion +resharper_convert_conditional_ternary_expression_to_switch_expression_highlighting=hint +resharper_convert_if_do_to_while_highlighting=suggestion +resharper_convert_if_statement_to_conditional_ternary_expression_highlighting=suggestion +resharper_convert_if_statement_to_null_coalescing_assignment_highlighting=suggestion +resharper_convert_if_statement_to_null_coalescing_expression_highlighting=suggestion +resharper_convert_if_statement_to_return_statement_highlighting=hint +resharper_convert_if_statement_to_switch_expression_highlighting=hint +resharper_convert_if_statement_to_switch_statement_highlighting=hint +resharper_convert_if_to_or_expression_highlighting=suggestion +resharper_convert_nullable_to_short_form_highlighting=suggestion +resharper_convert_switch_statement_to_switch_expression_highlighting=hint +resharper_convert_to_auto_property_highlighting=suggestion +resharper_convert_to_auto_property_when_possible_highlighting=hint +resharper_convert_to_auto_property_with_private_setter_highlighting=hint +resharper_convert_to_compound_assignment_highlighting=hint +resharper_convert_to_constant_global_highlighting=hint +resharper_convert_to_constant_local_highlighting=hint +resharper_convert_to_lambda_expression_highlighting=suggestion +resharper_convert_to_lambda_expression_when_possible_highlighting=none +resharper_convert_to_local_function_highlighting=suggestion +resharper_convert_to_null_coalescing_compound_assignment_highlighting=suggestion +resharper_convert_to_primary_constructor_highlighting=suggestion +resharper_convert_to_static_class_highlighting=suggestion +resharper_convert_to_using_declaration_highlighting=suggestion +resharper_convert_to_vb_auto_property_highlighting=suggestion +resharper_convert_to_vb_auto_property_when_possible_highlighting=hint +resharper_convert_to_vb_auto_property_with_private_setter_highlighting=hint +resharper_convert_type_check_pattern_to_null_check_highlighting=warning +resharper_convert_type_check_to_null_check_highlighting=warning +resharper_co_variant_array_conversion_highlighting=warning +resharper_cpp_abstract_class_without_specifier_highlighting=warning +resharper_cpp_abstract_final_class_highlighting=warning +resharper_cpp_abstract_virtual_function_call_in_ctor_highlighting=error +resharper_cpp_access_specifier_with_no_declarations_highlighting=suggestion +resharper_cpp_assigned_value_is_never_used_highlighting=warning +resharper_cpp_awaiter_type_is_not_class_highlighting=warning +resharper_cpp_bad_angle_brackets_spaces_highlighting=none +resharper_cpp_bad_braces_spaces_highlighting=none +resharper_cpp_bad_child_statement_indent_highlighting=none +resharper_cpp_bad_colon_spaces_highlighting=none +resharper_cpp_bad_comma_spaces_highlighting=none +resharper_cpp_bad_control_braces_indent_highlighting=none +resharper_cpp_bad_control_braces_line_breaks_highlighting=none +resharper_cpp_bad_declaration_braces_indent_highlighting=none +resharper_cpp_bad_declaration_braces_line_breaks_highlighting=none +resharper_cpp_bad_empty_braces_line_breaks_highlighting=none +resharper_cpp_bad_expression_braces_indent_highlighting=none +resharper_cpp_bad_expression_braces_line_breaks_highlighting=none +resharper_cpp_bad_indent_highlighting=none +resharper_cpp_bad_list_line_breaks_highlighting=none +resharper_cpp_bad_member_access_spaces_highlighting=none +resharper_cpp_bad_namespace_braces_indent_highlighting=none +resharper_cpp_bad_parens_line_breaks_highlighting=none +resharper_cpp_bad_parens_spaces_highlighting=none +resharper_cpp_bad_semicolon_spaces_highlighting=none +resharper_cpp_bad_spaces_after_keyword_highlighting=none +resharper_cpp_bad_square_brackets_spaces_highlighting=none +resharper_cpp_bad_switch_braces_indent_highlighting=none +resharper_cpp_bad_symbol_spaces_highlighting=none +resharper_cpp_boolean_increment_expression_highlighting=warning +resharper_cpp_boost_format_bad_code_highlighting=warning +resharper_cpp_boost_format_legacy_code_highlighting=suggestion +resharper_cpp_boost_format_mixed_args_highlighting=error +resharper_cpp_boost_format_too_few_args_highlighting=error +resharper_cpp_boost_format_too_many_args_highlighting=warning +resharper_cpp_clang_tidy_abseil_duration_addition_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_comparison_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_conversion_cast_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_division_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_factory_float_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_factory_scale_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_subtraction_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_unnecessary_conversion_highlighting=none +resharper_cpp_clang_tidy_abseil_faster_strsplit_delimiter_highlighting=none +resharper_cpp_clang_tidy_abseil_no_internal_dependencies_highlighting=none +resharper_cpp_clang_tidy_abseil_no_namespace_highlighting=none +resharper_cpp_clang_tidy_abseil_redundant_strcat_calls_highlighting=none +resharper_cpp_clang_tidy_abseil_string_find_startswith_highlighting=none +resharper_cpp_clang_tidy_abseil_string_find_str_contains_highlighting=none +resharper_cpp_clang_tidy_abseil_str_cat_append_highlighting=none +resharper_cpp_clang_tidy_abseil_time_comparison_highlighting=none +resharper_cpp_clang_tidy_abseil_time_subtraction_highlighting=none +resharper_cpp_clang_tidy_abseil_upgrade_duration_conversions_highlighting=none +resharper_cpp_clang_tidy_altera_id_dependent_backward_branch_highlighting=none +resharper_cpp_clang_tidy_altera_kernel_name_restriction_highlighting=none +resharper_cpp_clang_tidy_altera_single_work_item_barrier_highlighting=none +resharper_cpp_clang_tidy_altera_struct_pack_align_highlighting=none +resharper_cpp_clang_tidy_altera_unroll_loops_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_accept4_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_accept_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_creat_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_dup_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_epoll_create1_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_epoll_create_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_fopen_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_inotify_init1_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_inotify_init_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_memfd_create_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_open_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_pipe2_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_pipe_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_socket_highlighting=none +resharper_cpp_clang_tidy_android_comparison_in_temp_failure_retry_highlighting=none +resharper_cpp_clang_tidy_boost_use_to_string_highlighting=suggestion +resharper_cpp_clang_tidy_bugprone_argument_comment_highlighting=suggestion +resharper_cpp_clang_tidy_bugprone_assert_side_effect_highlighting=warning +resharper_cpp_clang_tidy_bugprone_bad_signal_to_kill_thread_highlighting=warning +resharper_cpp_clang_tidy_bugprone_bool_pointer_implicit_conversion_highlighting=none +resharper_cpp_clang_tidy_bugprone_branch_clone_highlighting=warning +resharper_cpp_clang_tidy_bugprone_copy_constructor_init_highlighting=warning +resharper_cpp_clang_tidy_bugprone_dangling_handle_highlighting=warning +resharper_cpp_clang_tidy_bugprone_dynamic_static_initializers_highlighting=warning +resharper_cpp_clang_tidy_bugprone_easily_swappable_parameters_highlighting=none +resharper_cpp_clang_tidy_bugprone_exception_escape_highlighting=none +resharper_cpp_clang_tidy_bugprone_fold_init_type_highlighting=warning +resharper_cpp_clang_tidy_bugprone_forwarding_reference_overload_highlighting=warning +resharper_cpp_clang_tidy_bugprone_forward_declaration_namespace_highlighting=warning +resharper_cpp_clang_tidy_bugprone_implicit_widening_of_multiplication_result_highlighting=warning +resharper_cpp_clang_tidy_bugprone_inaccurate_erase_highlighting=warning +resharper_cpp_clang_tidy_bugprone_incorrect_roundings_highlighting=warning +resharper_cpp_clang_tidy_bugprone_infinite_loop_highlighting=warning +resharper_cpp_clang_tidy_bugprone_integer_division_highlighting=warning +resharper_cpp_clang_tidy_bugprone_lambda_function_name_highlighting=warning +resharper_cpp_clang_tidy_bugprone_macro_parentheses_highlighting=warning +resharper_cpp_clang_tidy_bugprone_macro_repeated_side_effects_highlighting=warning +resharper_cpp_clang_tidy_bugprone_misplaced_operator_in_strlen_in_alloc_highlighting=warning +resharper_cpp_clang_tidy_bugprone_misplaced_pointer_arithmetic_in_alloc_highlighting=warning +resharper_cpp_clang_tidy_bugprone_misplaced_widening_cast_highlighting=warning +resharper_cpp_clang_tidy_bugprone_move_forwarding_reference_highlighting=warning +resharper_cpp_clang_tidy_bugprone_multiple_statement_macro_highlighting=warning +resharper_cpp_clang_tidy_bugprone_narrowing_conversions_highlighting=warning +resharper_cpp_clang_tidy_bugprone_not_null_terminated_result_highlighting=warning +resharper_cpp_clang_tidy_bugprone_no_escape_highlighting=warning +resharper_cpp_clang_tidy_bugprone_parent_virtual_call_highlighting=warning +resharper_cpp_clang_tidy_bugprone_posix_return_highlighting=warning +resharper_cpp_clang_tidy_bugprone_redundant_branch_condition_highlighting=warning +resharper_cpp_clang_tidy_bugprone_reserved_identifier_highlighting=warning +resharper_cpp_clang_tidy_bugprone_signal_handler_highlighting=warning +resharper_cpp_clang_tidy_bugprone_signed_char_misuse_highlighting=warning +resharper_cpp_clang_tidy_bugprone_sizeof_container_highlighting=warning +resharper_cpp_clang_tidy_bugprone_sizeof_expression_highlighting=warning +resharper_cpp_clang_tidy_bugprone_spuriously_wake_up_functions_highlighting=warning +resharper_cpp_clang_tidy_bugprone_string_constructor_highlighting=warning +resharper_cpp_clang_tidy_bugprone_string_integer_assignment_highlighting=warning +resharper_cpp_clang_tidy_bugprone_string_literal_with_embedded_nul_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_enum_usage_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_include_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_memset_usage_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_missing_comma_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_semicolon_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_string_compare_highlighting=warning +resharper_cpp_clang_tidy_bugprone_swapped_arguments_highlighting=warning +resharper_cpp_clang_tidy_bugprone_terminating_continue_highlighting=warning +resharper_cpp_clang_tidy_bugprone_throw_keyword_missing_highlighting=warning +resharper_cpp_clang_tidy_bugprone_too_small_loop_variable_highlighting=warning +resharper_cpp_clang_tidy_bugprone_undefined_memory_manipulation_highlighting=warning +resharper_cpp_clang_tidy_bugprone_undelegated_constructor_highlighting=warning +resharper_cpp_clang_tidy_bugprone_unhandled_exception_at_new_highlighting=none +resharper_cpp_clang_tidy_bugprone_unhandled_self_assignment_highlighting=warning +resharper_cpp_clang_tidy_bugprone_unused_raii_highlighting=warning +resharper_cpp_clang_tidy_bugprone_unused_return_value_highlighting=warning +resharper_cpp_clang_tidy_bugprone_use_after_move_highlighting=warning +resharper_cpp_clang_tidy_bugprone_virtual_near_miss_highlighting=suggestion +resharper_cpp_clang_tidy_cert_con36_c_highlighting=none +resharper_cpp_clang_tidy_cert_con54_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl03_c_highlighting=none +resharper_cpp_clang_tidy_cert_dcl16_c_highlighting=none +resharper_cpp_clang_tidy_cert_dcl21_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl37_c_highlighting=none +resharper_cpp_clang_tidy_cert_dcl50_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl51_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl54_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl58_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_dcl59_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_env33_c_highlighting=none +resharper_cpp_clang_tidy_cert_err09_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_err34_c_highlighting=suggestion +resharper_cpp_clang_tidy_cert_err52_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_err58_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_err60_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_err61_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_fio38_c_highlighting=none +resharper_cpp_clang_tidy_cert_flp30_c_highlighting=warning +resharper_cpp_clang_tidy_cert_mem57_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_msc30_c_highlighting=none +resharper_cpp_clang_tidy_cert_msc32_c_highlighting=none +resharper_cpp_clang_tidy_cert_msc50_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_msc51_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_oop11_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_oop54_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_oop57_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_oop58_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_pos44_c_highlighting=none +resharper_cpp_clang_tidy_cert_pos47_c_highlighting=none +resharper_cpp_clang_tidy_cert_sig30_c_highlighting=none +resharper_cpp_clang_tidy_cert_str34_c_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_google_g_test_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_llvm_cast_value_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_llvm_return_value_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_std_c_library_functions_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_trust_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_builtin_builtin_functions_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_builtin_no_return_functions_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_call_and_message_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_call_and_message_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_divide_zero_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_dynamic_type_propagation_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_nonnil_string_constants_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_non_null_param_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_null_dereference_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_stack_address_escape_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_stack_addr_escape_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_undefined_binary_operator_result_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_array_subscript_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_assign_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_branch_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_captured_block_variable_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_undef_return_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_vla_size_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_inner_pointer_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_move_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_new_delete_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_new_delete_leaks_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_placement_new_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_pure_virtual_call_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_self_assignment_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_smart_ptr_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_virtual_call_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_deadcode_dead_stores_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_fuchsia_handle_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullability_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_dereferenced_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_passed_to_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_returned_from_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_null_passed_to_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_null_returned_from_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_cplusplus_uninitialized_object_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_cplusplus_virtual_call_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_mpi_mpi_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_osx_cocoa_localizability_empty_localization_context_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_osx_cocoa_localizability_non_localized_string_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_osx_os_object_c_style_cast_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_performance_gcd_antipattern_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_performance_padding_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_portability_unix_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_at_sync_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_autorelease_write_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_class_release_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_dealloc_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_incompatible_method_types_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_loops_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_missing_super_call_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_nil_arg_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_non_nil_return_value_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_ns_autorelease_pool_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_ns_error_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_obj_c_generics_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_retain_count_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_retain_count_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_run_loop_autorelease_leak_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_self_init_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_super_dealloc_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_unused_ivars_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_variadic_method_types_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_error_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_number_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_retain_release_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_containers_out_of_bounds_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_containers_pointer_sized_values_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_mig_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_ns_or_cf_error_deref_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_number_object_conversion_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_obj_c_property_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_os_object_retain_count_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_sec_keychain_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_float_loop_counter_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bcmp_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bcopy_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bzero_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_decode_value_of_obj_c_type_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_deprecated_or_unsafe_buffer_handling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_getpw_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_gets_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_mkstemp_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_mktemp_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_rand_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_security_syntax_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_strcpy_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_unchecked_return_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_vfork_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_bad_size_arg_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_c_string_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_null_arg_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_dynamic_memory_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_malloc_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_malloc_sizeof_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_mismatched_deallocator_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_vfork_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_copy_to_self_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_uninitialized_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_unterminated_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_valist_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_webkit_no_uncounted_member_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_webkit_ref_cntbl_base_virtual_dtor_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_webkit_uncounted_lambda_captures_checker_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_absolute_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_abstract_final_class_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_abstract_vbase_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_address_of_packed_member_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_address_of_temporary_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_aix_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_align_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_alloca_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_alloca_with_align_alignof_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_delete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_ellipsis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_member_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_reversed_operator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_analyzer_incompatible_plugin_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_anonymous_pack_parens_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_anon_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_bridge_casts_disallowed_in_nonarc_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_maybe_repeated_use_of_weak_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_non_pod_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_perform_selector_leaks_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_repeated_use_of_weak_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_retain_cycles_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_unsafe_retained_assign_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_argument_outside_range_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_array_bounds_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_array_bounds_pointer_arithmetic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_asm_operand_widths_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_assign_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_assume_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atimport_in_framework_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atomic_alignment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atomic_implicit_seq_cst_highlighting=suggestion +resharper_cpp_clang_tidy_clang_diagnostic_atomic_memory_ordering_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atomic_property_with_user_defined_accessor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_attribute_packed_for_bitfield_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_at_protocol_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_disable_vptr_sanitizer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_import_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_storage_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_var_id_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_availability_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_avr_rtlib_linking_quirks_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_backslash_newline_escape_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bad_function_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_binding_in_condition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bind_to_temporary_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitfield_constant_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitfield_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitfield_width_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitwise_conditional_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitwise_op_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_block_capture_autoreleasing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bool_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bool_operation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_braced_scalar_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bridge_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_builtin_assume_aligned_alignment_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_builtin_macro_redefined_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_builtin_memcpy_chk_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_builtin_requires_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c11_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c2x_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c99_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_c99_designator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c99_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_called_once_parameter_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_call_to_pure_virtual_from_ctor_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_align_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_calling_convention_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_function_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_of_sel_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_qual_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_qual_unrelated_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cf_string_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_char_subscripts_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_clang_cl_pch_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_class_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_class_varargs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cmse_union_leak_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_comma_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_comment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compare_distinct_pointer_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_completion_handler_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_complex_component_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_by_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_by_space_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_concepts_ts_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_conditional_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_conditional_uninitialized_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_config_macros_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constant_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constant_evaluated_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constant_logical_operand_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constexpr_not_const_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_consumed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_coroutine_missing_unhandled_exception_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_covered_switch_default_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_deprecated_writable_strings_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_reserved_user_defined_literal_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_extra_semi_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_inline_namespace_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_long_long_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_narrowing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_binary_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_mangling_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_designator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp2a_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp2a_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp2b_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_bind_to_temporary_copy_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_extra_semi_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_local_type_template_args_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_unnamed_type_template_args_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_binary_literal_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_cpp17_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_cpp17_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cstring_format_directive_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ctad_maybe_unsupported_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ctu_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cuda_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_custom_atomic_properties_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cxx_attribute_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_else_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_gsl_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_initializer_list_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_darwin_sdk_settings_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_date_time_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dealloc_in_category_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_debug_compression_unavailable_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_declaration_after_statement_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_defaulted_function_deleted_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_delegating_ctor_cycles_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_abstract_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_incomplete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_non_abstract_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_altivec_src_compat_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_anon_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_array_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_attributes_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_comma_subscript_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_user_provided_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_user_provided_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_declarations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_dynamic_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_compare_conditional_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_implementations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_increment_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_isa_usage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_pointer_introspection_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_pointer_introspection_perform_selector_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_register_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_this_capture_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_volatile_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_direct_ivar_access_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_disabled_macro_expansion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_distributed_object_modifiers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_division_by_zero_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dllexport_explicit_instantiation_decl_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dllimport_static_field_def_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dll_attribute_on_redeclaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_deprecated_sync_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_html_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_unknown_command_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_dollar_in_identifier_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_double_promotion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dtor_name_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dtor_typedef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_decl_specifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_method_arg_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_method_match_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_protocol_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dynamic_class_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dynamic_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_embedded_directive_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_body_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_decomposition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_init_stmt_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_translation_unit_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_encode_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_conditional_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_switch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_too_large_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_error_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_exceptions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_excess_initializers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_exit_time_destructors_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_expansion_to_defined_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_explicit_initialize_call_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_explicit_ownership_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_export_unnamed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_export_using_directive_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extern_c_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_extern_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_qualification_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_semi_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_semi_stmt_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_tokens_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_final_dtor_non_final_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_fixed_enum_extension_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_fixed_point_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_flag_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_flexible_array_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_equal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_overflow_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_zero_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_extra_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_insufficient_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_invalid_specifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_nonliteral_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_non_iso_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_security_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_type_confusion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_zero_length_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_fortify_source_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_for_loop_analysis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_four_char_constants_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_framework_include_private_from_public_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_frame_address_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_frame_larger_than_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_free_nonheap_object_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_function_def_in_objc_container_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_function_multiversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gcc_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_global_constructors_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_global_isel_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_alignof_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_anonymous_struct_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_gnu_array_member_paren_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_auto_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_binary_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_case_range_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_complex_integer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_compound_literal_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_conditional_omitted_operand_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_designator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_empty_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_empty_struct_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_flexible_array_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_flexible_array_union_member_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_folding_constant_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_imaginary_constant_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_include_next_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_inline_cpp_without_extern_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_label_as_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_redeclared_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_statement_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_static_float_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_string_literal_operator_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_union_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_variable_sized_type_not_at_end_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_zero_variadic_macro_arguments_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_header_guard_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_header_hygiene_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_hip_only_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_idiomatic_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_attributes_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ignored_availability_without_sdk_settings_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ignored_optimization_argument_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragmas_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragma_intrinsic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragma_optimize_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ignored_qualifiers_highlighting=suggestion +resharper_cpp_clang_tidy_clang_diagnostic_implicitly_unsigned_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_atomic_properties_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_const_int_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_conversion_floating_point_to_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_exception_spec_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_fallthrough_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_fallthrough_per_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_fixed_point_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_function_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_retain_self_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_import_preprocessor_directive_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inaccessible_base_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_include_next_absolute_path_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_include_next_outside_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_function_pointer_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_library_redeclaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_ms_struct_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_pointer_types_discards_qualifiers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_pointer_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_property_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_sysroot_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_framework_module_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_implementation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_setjmp_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_umbrella_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_dllimport_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_missing_destructor_override_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_missing_override_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_increment_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_independent_class_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_infinite_recursion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_initializer_overrides_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_injected_class_name_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inline_asm_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inline_namespace_reopened_noninline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inline_new_delete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_instantiation_after_specialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_integer_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_interrupt_service_routine_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_in_bool_context_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_to_pointer_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_to_void_pointer_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_constexpr_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_iboutlet_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_initializer_from_system_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_ios_deployment_target_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_noreturn_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_no_builtin_names_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_offsetof_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_or_nonexistent_directory_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_partial_specialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_pp_token_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_source_encoding_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_token_paste_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_jump_seh_finally_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_keyword_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_keyword_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_knr_promoted_parameter_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_language_extension_token_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_large_by_value_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_literal_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_literal_range_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_local_type_template_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_logical_not_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_logical_op_parentheses_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_long_long_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_macro_redefined_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_main_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_main_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_malformed_warning_check_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_many_braces_around_scalar_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_max_tokens_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_max_unsigned_zero_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_memset_transposed_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_memsize_comparison_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_method_signatures_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_abstract_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_anon_tag_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_charize_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_comment_paste_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_const_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_cpp_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_default_arg_redefinition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_drectve_section_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_end_of_file_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_enum_forward_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_enum_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_exists_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_explicit_constructor_call_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_extra_qualification_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_fixed_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_flexible_array_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_goto_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_inaccessible_base_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_include_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_mutable_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_pure_definition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_redeclare_static_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_sealed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_static_assert_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_template_shadow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_union_member_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_unqualified_friend_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_using_decl_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_void_pseudo_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_misleading_indentation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_new_delete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_parameter_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_return_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_tags_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_braces_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_constinit_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_declarations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_field_initializers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_method_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_noescape_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_noreturn_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_prototypes_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_missing_prototype_for_cc_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_selector_name_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_sysroot_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_variable_declarations_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_misspelled_assumption_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_modules_ambiguous_internal_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_modules_import_nested_redundant_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_conflict_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_file_config_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_file_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_import_in_extern_c_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_msvc_not_found_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_multichar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_multiple_move_vbase_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nested_anon_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_newline_eof_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_new_returns_null_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_noderef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonnull_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonportable_include_path_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonportable_system_include_path_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonportable_vector_initialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nontrivial_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_c_typedef_for_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_literal_null_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_modular_include_in_framework_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_modular_include_in_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_pod_varargs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_power_of_two_alignment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nsconsumed_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nsreturns_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ns_object_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_completeness_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_completeness_on_arrays_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_declspec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_inferred_on_nested_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullable_to_nonnull_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_arithmetic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_character_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_dereference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_pointer_arithmetic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_pointer_subtraction_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_odr_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_old_style_cast_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_opencl_unsupported_rgba_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp51_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_clauses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_loop_form_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_mapping_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_target_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_option_ignored_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ordered_compare_function_pointers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_out_of_line_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_out_of_scope_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overlength_strings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overloaded_shift_op_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overloaded_virtual_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_override_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_override_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overriding_method_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overriding_t_option_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_over_aligned_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_packed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_padded_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_parentheses_equality_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pass_failed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pch_date_time_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pedantic_core_features_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pessimizing_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_arith_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_bool_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_integer_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_sign_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_to_enum_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_to_int_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_poison_system_directories_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_potentially_evaluated_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragmas_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_clang_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_messages_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_once_outside_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_pack_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_pack_suspicious_include_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_system_header_outside_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_predefined_identifier_outside_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pre_c2x_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_c2x_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp14_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp14_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp17_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp17_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp20_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp20_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp2b_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp2b_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_openmp51_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_private_extern_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_private_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_private_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_missing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_out_of_date_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_unprofiled_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_property_access_dot_syntax_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_property_attribute_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_protocol_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_protocol_property_synthesis_ambiguity_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_psabi_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_qualified_void_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_quoted_include_in_framework_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_range_loop_analysis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_range_loop_bind_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_range_loop_construct_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_readonly_iboutlet_property_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_receiver_expr_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_receiver_forward_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_redeclared_class_member_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_redundant_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_redundant_parens_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_register_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reinterpret_base_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reorder_ctor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reorder_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reorder_init_list_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_requires_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_requires_super_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_identifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_id_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_macro_identifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_user_defined_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_retained_language_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_stack_address_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_std_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_type_c_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_rewrite_not_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_section_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_selector_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_selector_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_assign_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_assign_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_assign_overloaded_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_semicolon_before_method_body_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sentinel_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_serialized_diagnostics_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_in_constructor_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_in_constructor_modified_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_shadow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_ivar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_uncaptured_local_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_shift_count_negative_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_count_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_negative_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_op_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_sign_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shorten64_to32_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_signed_enum_bitfield_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_signed_unsigned_wchar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sign_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sign_conversion_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_argument_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_decay_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_div_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_pointer_div_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_pointer_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_slash_u_filename_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_slh_asm_goto_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_sometimes_uninitialized_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_source_uses_openmp_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_spir_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_static_float_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_inline_explicit_instantiation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_in_inline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_local_in_inline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_self_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_stdlibcxx_not_found_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_strict_prototypes_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_strict_selector_match_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_concatenation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_plus_char_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_plus_int_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_strlcpy_strlcat_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_strncat_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_suggest_destructor_override_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_suggest_override_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_super_class_method_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_suspicious_bzero_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_switch_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_switch_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_switch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sync_fetch_and_nand_semantics_changed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_bitwise_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_in_range_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_out_of_range_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_objc_bool_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_overlap_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_pointer_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_type_limit_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_undefined_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_char_zero_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_enum_zero_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_zero_compare_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_tautological_value_range_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tentative_definition_incomplete_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_analysis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_attributes_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_beta_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_negative_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_precise_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_verbose_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_trigraphs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_typedef_redefinition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_typename_missing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_type_safety_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unable_to_open_stats_file_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unavailable_declarations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undeclared_selector_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_bool_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_func_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_inline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_internal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_internal_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_reinterpret_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_var_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undef_prefix_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_underaligned_exception_object_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unevaluated_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unguarded_availability_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unguarded_availability_new_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_homoglyph_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_whitespace_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_zero_width_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_uninitialized_const_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_uninitialized_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_argument_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_attributes_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unknown_cuda_version_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unknown_escape_sequence_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_pragmas_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unknown_sanitizers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_warning_option_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unnamed_type_template_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unneeded_internal_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unneeded_member_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_break_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_loop_increment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_return_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsequenced_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_abs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_availability_guard_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_cb_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_dll_base_class_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_friend_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_gpopt_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_nan_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_target_opt_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_visibility_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unusable_partial_specialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_but_set_parameter_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_but_set_variable_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_comparison_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_const_variable_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_exception_parameter_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_getter_return_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_label_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_lambda_capture_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_local_typedef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_macros_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_member_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_parameter_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_private_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_property_ivar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_result_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_variable_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_volatile_lvalue_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_used_but_marked_unused_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_user_defined_literals_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_user_defined_warnings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_varargs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_variadic_macros_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vector_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vec_elem_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vexing_parse_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_visibility_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vla_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vla_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_void_pointer_to_enum_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_void_pointer_to_int_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_void_ptr_dereference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_warnings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_wasm_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_weak_template_vtables_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_weak_vtables_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_writable_strings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_xor_used_as_pow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_zero_as_null_pointer_constant_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_zero_length_array_highlighting=warning +resharper_cpp_clang_tidy_concurrency_mt_unsafe_highlighting=warning +resharper_cpp_clang_tidy_concurrency_thread_canceltype_asynchronous_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_c_arrays_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_goto_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_magic_numbers_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_non_const_global_variables_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_c_copy_assignment_signature_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_explicit_virtual_functions_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_init_variables_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_interfaces_global_init_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_macro_usage_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_narrowing_conversions_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_non_private_member_variables_in_classes_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_no_malloc_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_owning_memory_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_prefer_member_initializer_highlighting=suggestion +resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_array_to_pointer_decay_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_constant_array_index_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_pointer_arithmetic_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_const_cast_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_cstyle_cast_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_member_init_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_reinterpret_cast_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_static_cast_downcast_highlighting=suggestion +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_union_access_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_vararg_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_slicing_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_special_member_functions_highlighting=suggestion +resharper_cpp_clang_tidy_darwin_avoid_spinlock_highlighting=none +resharper_cpp_clang_tidy_darwin_dispatch_once_nonstatic_highlighting=none +resharper_cpp_clang_tidy_fuchsia_default_arguments_calls_highlighting=none +resharper_cpp_clang_tidy_fuchsia_default_arguments_declarations_highlighting=none +resharper_cpp_clang_tidy_fuchsia_header_anon_namespaces_highlighting=none +resharper_cpp_clang_tidy_fuchsia_multiple_inheritance_highlighting=none +resharper_cpp_clang_tidy_fuchsia_overloaded_operator_highlighting=none +resharper_cpp_clang_tidy_fuchsia_statically_constructed_objects_highlighting=none +resharper_cpp_clang_tidy_fuchsia_trailing_return_highlighting=none +resharper_cpp_clang_tidy_fuchsia_virtual_inheritance_highlighting=none +resharper_cpp_clang_tidy_google_build_explicit_make_pair_highlighting=none +resharper_cpp_clang_tidy_google_build_namespaces_highlighting=none +resharper_cpp_clang_tidy_google_build_using_namespace_highlighting=none +resharper_cpp_clang_tidy_google_default_arguments_highlighting=none +resharper_cpp_clang_tidy_google_explicit_constructor_highlighting=none +resharper_cpp_clang_tidy_google_global_names_in_headers_highlighting=none +resharper_cpp_clang_tidy_google_objc_avoid_nsobject_new_highlighting=none +resharper_cpp_clang_tidy_google_objc_avoid_throwing_exception_highlighting=none +resharper_cpp_clang_tidy_google_objc_function_naming_highlighting=none +resharper_cpp_clang_tidy_google_objc_global_variable_declaration_highlighting=none +resharper_cpp_clang_tidy_google_readability_avoid_underscore_in_googletest_name_highlighting=none +resharper_cpp_clang_tidy_google_readability_braces_around_statements_highlighting=none +resharper_cpp_clang_tidy_google_readability_casting_highlighting=none +resharper_cpp_clang_tidy_google_readability_function_size_highlighting=none +resharper_cpp_clang_tidy_google_readability_namespace_comments_highlighting=none +resharper_cpp_clang_tidy_google_readability_todo_highlighting=none +resharper_cpp_clang_tidy_google_runtime_int_highlighting=none +resharper_cpp_clang_tidy_google_runtime_operator_highlighting=warning +resharper_cpp_clang_tidy_google_upgrade_googletest_case_highlighting=suggestion +resharper_cpp_clang_tidy_hicpp_avoid_c_arrays_highlighting=none +resharper_cpp_clang_tidy_hicpp_avoid_goto_highlighting=warning +resharper_cpp_clang_tidy_hicpp_braces_around_statements_highlighting=none +resharper_cpp_clang_tidy_hicpp_deprecated_headers_highlighting=none +resharper_cpp_clang_tidy_hicpp_exception_baseclass_highlighting=suggestion +resharper_cpp_clang_tidy_hicpp_explicit_conversions_highlighting=none +resharper_cpp_clang_tidy_hicpp_function_size_highlighting=none +resharper_cpp_clang_tidy_hicpp_invalid_access_moved_highlighting=none +resharper_cpp_clang_tidy_hicpp_member_init_highlighting=none +resharper_cpp_clang_tidy_hicpp_move_const_arg_highlighting=none +resharper_cpp_clang_tidy_hicpp_multiway_paths_covered_highlighting=warning +resharper_cpp_clang_tidy_hicpp_named_parameter_highlighting=none +resharper_cpp_clang_tidy_hicpp_new_delete_operators_highlighting=none +resharper_cpp_clang_tidy_hicpp_noexcept_move_highlighting=none +resharper_cpp_clang_tidy_hicpp_no_array_decay_highlighting=none +resharper_cpp_clang_tidy_hicpp_no_assembler_highlighting=none +resharper_cpp_clang_tidy_hicpp_no_malloc_highlighting=none +resharper_cpp_clang_tidy_hicpp_signed_bitwise_highlighting=none +resharper_cpp_clang_tidy_hicpp_special_member_functions_highlighting=none +resharper_cpp_clang_tidy_hicpp_static_assert_highlighting=none +resharper_cpp_clang_tidy_hicpp_undelegated_constructor_highlighting=none +resharper_cpp_clang_tidy_hicpp_uppercase_literal_suffix_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_auto_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_emplace_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_equals_default_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_equals_delete_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_noexcept_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_nullptr_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_override_highlighting=none +resharper_cpp_clang_tidy_hicpp_vararg_highlighting=none +resharper_cpp_clang_tidy_highlighting_highlighting=suggestion +resharper_cpp_clang_tidy_linuxkernel_must_check_errs_highlighting=warning +resharper_cpp_clang_tidy_llvmlibc_callee_namespace_highlighting=none +resharper_cpp_clang_tidy_llvmlibc_implementation_in_namespace_highlighting=none +resharper_cpp_clang_tidy_llvmlibc_restrict_system_libc_headers_highlighting=none +resharper_cpp_clang_tidy_llvm_else_after_return_highlighting=none +resharper_cpp_clang_tidy_llvm_header_guard_highlighting=none +resharper_cpp_clang_tidy_llvm_include_order_highlighting=none +resharper_cpp_clang_tidy_llvm_namespace_comment_highlighting=none +resharper_cpp_clang_tidy_llvm_prefer_isa_or_dyn_cast_in_conditionals_highlighting=none +resharper_cpp_clang_tidy_llvm_prefer_register_over_unsigned_highlighting=suggestion +resharper_cpp_clang_tidy_llvm_qualified_auto_highlighting=none +resharper_cpp_clang_tidy_llvm_twine_local_highlighting=none +resharper_cpp_clang_tidy_misc_definitions_in_headers_highlighting=none +resharper_cpp_clang_tidy_misc_misplaced_const_highlighting=warning +resharper_cpp_clang_tidy_misc_new_delete_overloads_highlighting=warning +resharper_cpp_clang_tidy_misc_non_copyable_objects_highlighting=warning +resharper_cpp_clang_tidy_misc_non_private_member_variables_in_classes_highlighting=none +resharper_cpp_clang_tidy_misc_no_recursion_highlighting=none +resharper_cpp_clang_tidy_misc_redundant_expression_highlighting=warning +resharper_cpp_clang_tidy_misc_static_assert_highlighting=suggestion +resharper_cpp_clang_tidy_misc_throw_by_value_catch_by_reference_highlighting=warning +resharper_cpp_clang_tidy_misc_unconventional_assign_operator_highlighting=warning +resharper_cpp_clang_tidy_misc_uniqueptr_reset_release_highlighting=suggestion +resharper_cpp_clang_tidy_misc_unused_alias_decls_highlighting=suggestion +resharper_cpp_clang_tidy_misc_unused_parameters_highlighting=none +resharper_cpp_clang_tidy_misc_unused_using_decls_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_avoid_bind_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_avoid_c_arrays_highlighting=none +resharper_cpp_clang_tidy_modernize_concat_nested_namespaces_highlighting=none +resharper_cpp_clang_tidy_modernize_deprecated_headers_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_deprecated_ios_base_aliases_highlighting=warning +resharper_cpp_clang_tidy_modernize_loop_convert_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_make_shared_highlighting=none +resharper_cpp_clang_tidy_modernize_make_unique_highlighting=none +resharper_cpp_clang_tidy_modernize_pass_by_value_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_raw_string_literal_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_redundant_void_arg_highlighting=none +resharper_cpp_clang_tidy_modernize_replace_auto_ptr_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_replace_disallow_copy_and_assign_macro_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_replace_random_shuffle_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_return_braced_init_list_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_shrink_to_fit_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_unary_static_assert_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_auto_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_bool_literals_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_default_member_init_highlighting=none +resharper_cpp_clang_tidy_modernize_use_emplace_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_equals_default_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_equals_delete_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_nodiscard_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_noexcept_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_nullptr_highlighting=none +resharper_cpp_clang_tidy_modernize_use_override_highlighting=none +resharper_cpp_clang_tidy_modernize_use_trailing_return_type_highlighting=none +resharper_cpp_clang_tidy_modernize_use_transparent_functors_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_uncaught_exceptions_highlighting=warning +resharper_cpp_clang_tidy_modernize_use_using_highlighting=none +resharper_cpp_clang_tidy_mpi_buffer_deref_highlighting=warning +resharper_cpp_clang_tidy_mpi_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_objc_avoid_nserror_init_highlighting=warning +resharper_cpp_clang_tidy_objc_dealloc_in_category_highlighting=warning +resharper_cpp_clang_tidy_objc_forbidden_subclassing_highlighting=warning +resharper_cpp_clang_tidy_objc_missing_hash_highlighting=warning +resharper_cpp_clang_tidy_objc_nsinvocation_argument_lifetime_highlighting=warning +resharper_cpp_clang_tidy_objc_property_declaration_highlighting=warning +resharper_cpp_clang_tidy_objc_super_self_highlighting=warning +resharper_cpp_clang_tidy_openmp_exception_escape_highlighting=warning +resharper_cpp_clang_tidy_openmp_use_default_none_highlighting=warning +resharper_cpp_clang_tidy_performance_faster_string_find_highlighting=suggestion +resharper_cpp_clang_tidy_performance_for_range_copy_highlighting=suggestion +resharper_cpp_clang_tidy_performance_implicit_conversion_in_loop_highlighting=suggestion +resharper_cpp_clang_tidy_performance_inefficient_algorithm_highlighting=suggestion +resharper_cpp_clang_tidy_performance_inefficient_string_concatenation_highlighting=suggestion +resharper_cpp_clang_tidy_performance_inefficient_vector_operation_highlighting=suggestion +resharper_cpp_clang_tidy_performance_move_constructor_init_highlighting=warning +resharper_cpp_clang_tidy_performance_move_const_arg_highlighting=suggestion +resharper_cpp_clang_tidy_performance_noexcept_move_constructor_highlighting=none +resharper_cpp_clang_tidy_performance_no_automatic_move_highlighting=warning +resharper_cpp_clang_tidy_performance_no_int_to_ptr_highlighting=warning +resharper_cpp_clang_tidy_performance_trivially_destructible_highlighting=suggestion +resharper_cpp_clang_tidy_performance_type_promotion_in_math_fn_highlighting=suggestion +resharper_cpp_clang_tidy_performance_unnecessary_copy_initialization_highlighting=suggestion +resharper_cpp_clang_tidy_performance_unnecessary_value_param_highlighting=suggestion +resharper_cpp_clang_tidy_portability_restrict_system_includes_highlighting=none +resharper_cpp_clang_tidy_portability_simd_intrinsics_highlighting=none +resharper_cpp_clang_tidy_readability_avoid_const_params_in_decls_highlighting=none +resharper_cpp_clang_tidy_readability_braces_around_statements_highlighting=none +resharper_cpp_clang_tidy_readability_const_return_type_highlighting=none +resharper_cpp_clang_tidy_readability_container_size_empty_highlighting=suggestion +resharper_cpp_clang_tidy_readability_convert_member_functions_to_static_highlighting=none +resharper_cpp_clang_tidy_readability_delete_null_pointer_highlighting=suggestion +resharper_cpp_clang_tidy_readability_else_after_return_highlighting=none +resharper_cpp_clang_tidy_readability_function_cognitive_complexity_highlighting=none +resharper_cpp_clang_tidy_readability_function_size_highlighting=none +resharper_cpp_clang_tidy_readability_identifier_naming_highlighting=none +resharper_cpp_clang_tidy_readability_implicit_bool_conversion_highlighting=none +resharper_cpp_clang_tidy_readability_inconsistent_declaration_parameter_name_highlighting=suggestion +resharper_cpp_clang_tidy_readability_isolate_declaration_highlighting=none +resharper_cpp_clang_tidy_readability_magic_numbers_highlighting=none +resharper_cpp_clang_tidy_readability_make_member_function_const_highlighting=none +resharper_cpp_clang_tidy_readability_misleading_indentation_highlighting=none +resharper_cpp_clang_tidy_readability_misplaced_array_index_highlighting=suggestion +resharper_cpp_clang_tidy_readability_named_parameter_highlighting=none +resharper_cpp_clang_tidy_readability_non_const_parameter_highlighting=none +resharper_cpp_clang_tidy_readability_qualified_auto_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_access_specifiers_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_control_flow_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_declaration_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_function_ptr_dereference_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_member_init_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_preprocessor_highlighting=warning +resharper_cpp_clang_tidy_readability_redundant_smartptr_get_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_string_cstr_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_string_init_highlighting=suggestion +resharper_cpp_clang_tidy_readability_simplify_boolean_expr_highlighting=none +resharper_cpp_clang_tidy_readability_simplify_subscript_expr_highlighting=warning +resharper_cpp_clang_tidy_readability_static_accessed_through_instance_highlighting=suggestion +resharper_cpp_clang_tidy_readability_static_definition_in_anonymous_namespace_highlighting=none +resharper_cpp_clang_tidy_readability_string_compare_highlighting=warning +resharper_cpp_clang_tidy_readability_suspicious_call_argument_highlighting=warning +resharper_cpp_clang_tidy_readability_uniqueptr_delete_release_highlighting=suggestion +resharper_cpp_clang_tidy_readability_uppercase_literal_suffix_highlighting=none +resharper_cpp_clang_tidy_readability_use_anyofallof_highlighting=suggestion +resharper_cpp_clang_tidy_zircon_temporary_objects_highlighting=none +resharper_cpp_class_can_be_final_highlighting=hint +resharper_cpp_class_disallow_lazy_merging_highlighting=warning +resharper_cpp_class_is_incomplete_highlighting=warning +resharper_cpp_class_needs_constructor_because_of_uninitialized_member_highlighting=warning +resharper_cpp_class_never_used_highlighting=warning +resharper_cpp_compile_time_constant_can_be_replaced_with_boolean_constant_highlighting=suggestion +resharper_cpp_const_parameter_in_declaration_highlighting=suggestion +resharper_cpp_const_value_function_return_type_highlighting=suggestion +resharper_cpp_coroutine_call_resolve_error_highlighting=warning +resharper_cpp_cv_qualifier_can_not_be_applied_to_reference_highlighting=warning +resharper_cpp_c_style_cast_highlighting=suggestion +resharper_cpp_declaration_hides_local_highlighting=warning +resharper_cpp_declaration_hides_uncaptured_local_highlighting=hint +resharper_cpp_declaration_specifier_without_declarators_highlighting=warning +resharper_cpp_declarator_disambiguated_as_function_highlighting=warning +resharper_cpp_declarator_never_used_highlighting=warning +resharper_cpp_declarator_used_before_initialization_highlighting=error +resharper_cpp_defaulted_special_member_function_is_implicitly_deleted_highlighting=warning +resharper_cpp_default_case_not_handled_in_switch_statement_highlighting=warning +resharper_cpp_default_initialization_with_no_user_constructor_highlighting=warning +resharper_cpp_default_is_used_as_identifier_highlighting=warning +resharper_cpp_deleting_void_pointer_highlighting=warning +resharper_cpp_dependent_template_without_template_keyword_highlighting=warning +resharper_cpp_dependent_type_without_typename_keyword_highlighting=warning +resharper_cpp_deprecated_entity_highlighting=warning +resharper_cpp_deprecated_register_storage_class_specifier_highlighting=warning +resharper_cpp_dereference_operator_limit_exceeded_highlighting=warning +resharper_cpp_discarded_postfix_operator_result_highlighting=suggestion +resharper_cpp_doxygen_syntax_error_highlighting=warning +resharper_cpp_doxygen_undocumented_parameter_highlighting=suggestion +resharper_cpp_doxygen_unresolved_reference_highlighting=warning +resharper_cpp_empty_declaration_highlighting=warning +resharper_cpp_enforce_cv_qualifiers_order_highlighting=none +resharper_cpp_enforce_cv_qualifiers_placement_highlighting=none +resharper_cpp_enforce_do_statement_braces_highlighting=none +resharper_cpp_enforce_for_statement_braces_highlighting=none +resharper_cpp_enforce_function_declaration_style_highlighting=none +resharper_cpp_enforce_if_statement_braces_highlighting=none +resharper_cpp_enforce_nested_namespaces_style_highlighting=hint +resharper_cpp_enforce_overriding_destructor_style_highlighting=suggestion +resharper_cpp_enforce_overriding_function_style_highlighting=suggestion +resharper_cpp_enforce_type_alias_code_style_highlighting=none +resharper_cpp_enforce_while_statement_braces_highlighting=none +resharper_cpp_entity_assigned_but_no_read_highlighting=warning +resharper_cpp_entity_used_only_in_unevaluated_context_highlighting=warning +resharper_cpp_enumerator_never_used_highlighting=warning +resharper_cpp_equal_operands_in_binary_expression_highlighting=warning +resharper_cpp_explicit_specialization_in_non_namespace_scope_highlighting=warning +resharper_cpp_expression_without_side_effects_highlighting=warning +resharper_cpp_final_function_in_final_class_highlighting=suggestion +resharper_cpp_final_non_overriding_virtual_function_highlighting=suggestion +resharper_cpp_for_loop_can_be_replaced_with_while_highlighting=suggestion +resharper_cpp_functional_style_cast_highlighting=suggestion +resharper_cpp_function_doesnt_return_value_highlighting=warning +resharper_cpp_function_is_not_implemented_highlighting=warning +resharper_cpp_header_has_been_already_included_highlighting=hint +resharper_cpp_hidden_function_highlighting=warning +resharper_cpp_hiding_function_highlighting=warning +resharper_cpp_identical_operands_in_binary_expression_highlighting=warning +resharper_cpp_if_can_be_replaced_by_constexpr_if_highlighting=suggestion +resharper_cpp_implicit_default_constructor_not_available_highlighting=warning +resharper_cpp_incompatible_pointer_conversion_highlighting=warning +resharper_cpp_incomplete_switch_statement_highlighting=warning +resharper_cpp_inconsistent_naming_highlighting=hint +resharper_cpp_incorrect_blank_lines_near_braces_highlighting=none +resharper_cpp_initialized_value_is_always_rewritten_highlighting=warning +resharper_cpp_integral_to_pointer_conversion_highlighting=warning +resharper_cpp_invalid_line_continuation_highlighting=warning +resharper_cpp_join_declaration_and_assignment_highlighting=suggestion +resharper_cpp_lambda_capture_never_used_highlighting=warning +resharper_cpp_local_variable_may_be_const_highlighting=suggestion +resharper_cpp_local_variable_might_not_be_initialized_highlighting=warning +resharper_cpp_local_variable_with_non_trivial_dtor_is_never_used_highlighting=none +resharper_cpp_long_float_highlighting=warning +resharper_cpp_member_function_may_be_const_highlighting=suggestion +resharper_cpp_member_function_may_be_static_highlighting=suggestion +resharper_cpp_member_initializers_order_highlighting=suggestion +resharper_cpp_mismatched_class_tags_highlighting=warning +resharper_cpp_missing_blank_lines_highlighting=none +resharper_cpp_missing_include_guard_highlighting=warning +resharper_cpp_missing_indent_highlighting=none +resharper_cpp_missing_keyword_throw_highlighting=warning +resharper_cpp_missing_linebreak_highlighting=none +resharper_cpp_missing_space_highlighting=none +resharper_cpp_ms_ext_address_of_class_r_value_highlighting=warning +resharper_cpp_ms_ext_binding_r_value_to_lvalue_reference_highlighting=warning +resharper_cpp_ms_ext_copy_elision_in_copy_init_declarator_highlighting=warning +resharper_cpp_ms_ext_double_user_conversion_in_copy_init_highlighting=warning +resharper_cpp_ms_ext_not_initialized_static_const_local_var_highlighting=warning +resharper_cpp_ms_ext_reinterpret_cast_from_nullptr_highlighting=warning +resharper_cpp_multiple_spaces_highlighting=none +resharper_cpp_must_be_public_virtual_to_implement_interface_highlighting=warning +resharper_cpp_mutable_specifier_on_reference_member_highlighting=warning +resharper_cpp_nodiscard_function_without_return_value_highlighting=warning +resharper_cpp_non_exception_safe_resource_acquisition_highlighting=hint +resharper_cpp_non_explicit_conversion_operator_highlighting=hint +resharper_cpp_non_explicit_converting_constructor_highlighting=hint +resharper_cpp_non_inline_function_definition_in_header_file_highlighting=warning +resharper_cpp_non_inline_variable_definition_in_header_file_highlighting=warning +resharper_cpp_not_all_paths_return_value_highlighting=warning +resharper_cpp_no_discard_expression_highlighting=warning +resharper_cpp_object_member_might_not_be_initialized_highlighting=warning +resharper_cpp_outdent_is_off_prev_level_highlighting=none +resharper_cpp_out_parameter_must_be_written_highlighting=warning +resharper_cpp_parameter_may_be_const_highlighting=hint +resharper_cpp_parameter_may_be_const_ptr_or_ref_highlighting=suggestion +resharper_cpp_parameter_names_mismatch_highlighting=hint +resharper_cpp_parameter_never_used_highlighting=hint +resharper_cpp_parameter_value_is_reassigned_highlighting=warning +resharper_cpp_pointer_conversion_drops_qualifiers_highlighting=warning +resharper_cpp_pointer_to_integral_conversion_highlighting=warning +resharper_cpp_polymorphic_class_with_non_virtual_public_destructor_highlighting=warning +resharper_cpp_possibly_erroneous_empty_statements_highlighting=warning +resharper_cpp_possibly_uninitialized_member_highlighting=warning +resharper_cpp_possibly_unintended_object_slicing_highlighting=warning +resharper_cpp_precompiled_header_is_not_included_highlighting=error +resharper_cpp_precompiled_header_not_found_highlighting=error +resharper_cpp_printf_bad_format_highlighting=warning +resharper_cpp_printf_extra_arg_highlighting=warning +resharper_cpp_printf_missed_arg_highlighting=error +resharper_cpp_printf_risky_format_highlighting=warning +resharper_cpp_private_special_member_function_is_not_implemented_highlighting=warning +resharper_cpp_range_based_for_incompatible_reference_highlighting=warning +resharper_cpp_redefinition_of_default_argument_in_override_function_highlighting=warning +resharper_cpp_redundant_access_specifier_highlighting=hint +resharper_cpp_redundant_base_class_access_specifier_highlighting=hint +resharper_cpp_redundant_blank_lines_highlighting=none +resharper_cpp_redundant_boolean_expression_argument_highlighting=warning +resharper_cpp_redundant_cast_expression_highlighting=hint +resharper_cpp_redundant_const_specifier_highlighting=hint +resharper_cpp_redundant_control_flow_jump_highlighting=hint +resharper_cpp_redundant_elaborated_type_specifier_highlighting=hint +resharper_cpp_redundant_else_keyword_highlighting=hint +resharper_cpp_redundant_else_keyword_inside_compound_statement_highlighting=hint +resharper_cpp_redundant_empty_declaration_highlighting=hint +resharper_cpp_redundant_empty_statement_highlighting=hint +resharper_cpp_redundant_explicit_template_arguments_highlighting=hint +resharper_cpp_redundant_inline_specifier_highlighting=hint +resharper_cpp_redundant_lambda_parameter_list_highlighting=hint +resharper_cpp_redundant_linebreak_highlighting=none +resharper_cpp_redundant_member_initializer_highlighting=suggestion +resharper_cpp_redundant_namespace_definition_highlighting=suggestion +resharper_cpp_redundant_parentheses_highlighting=hint +resharper_cpp_redundant_qualifier_highlighting=hint +resharper_cpp_redundant_space_highlighting=none +resharper_cpp_redundant_static_specifier_on_member_allocation_function_highlighting=hint +resharper_cpp_redundant_template_keyword_highlighting=warning +resharper_cpp_redundant_typename_keyword_highlighting=warning +resharper_cpp_redundant_void_argument_list_highlighting=suggestion +resharper_cpp_reinterpret_cast_from_void_ptr_highlighting=suggestion +resharper_cpp_remove_redundant_braces_highlighting=none +resharper_cpp_replace_memset_with_zero_initialization_highlighting=suggestion +resharper_cpp_replace_tie_with_structured_binding_highlighting=suggestion +resharper_cpp_return_no_value_in_non_void_function_highlighting=warning +resharper_cpp_smart_pointer_vs_make_function_highlighting=suggestion +resharper_cpp_some_object_members_might_not_be_initialized_highlighting=warning +resharper_cpp_special_function_without_noexcept_specification_highlighting=warning +resharper_cpp_static_data_member_in_unnamed_struct_highlighting=warning +resharper_cpp_static_specifier_on_anonymous_namespace_member_highlighting=suggestion +resharper_cpp_string_literal_to_char_pointer_conversion_highlighting=warning +resharper_cpp_syntax_warning_highlighting=warning +resharper_cpp_tabs_and_spaces_mismatch_highlighting=none +resharper_cpp_tabs_are_disallowed_highlighting=none +resharper_cpp_tabs_outside_indent_highlighting=none +resharper_cpp_template_parameter_shadowing_highlighting=warning +resharper_cpp_this_arg_member_func_delegate_ctor_is_unsuported_by_dot_net_core_highlighting=none +resharper_cpp_throw_expression_can_be_replaced_with_rethrow_highlighting=warning +resharper_cpp_too_wide_scope_highlighting=suggestion +resharper_cpp_too_wide_scope_init_statement_highlighting=hint +resharper_cpp_type_alias_never_used_highlighting=warning +resharper_cpp_ue4_blueprint_callable_function_may_be_const_highlighting=hint +resharper_cpp_ue4_blueprint_callable_function_may_be_static_highlighting=hint +resharper_cpp_ue4_coding_standard_naming_violation_warning_highlighting=hint +resharper_cpp_ue4_coding_standard_u_class_naming_violation_error_highlighting=error +resharper_cpp_ue4_probable_memory_issues_with_u_objects_in_container_highlighting=warning +resharper_cpp_ue4_probable_memory_issues_with_u_object_highlighting=warning +resharper_cpp_ue_blueprint_callable_function_unused_highlighting=warning +resharper_cpp_ue_blueprint_implementable_event_not_implemented_highlighting=warning +resharper_cpp_ue_incorrect_engine_directory_highlighting=error +resharper_cpp_ue_non_existent_input_action_highlighting=warning +resharper_cpp_ue_non_existent_input_axis_highlighting=warning +resharper_cpp_ue_source_file_without_predefined_macros_highlighting=warning +resharper_cpp_ue_source_file_without_standard_library_highlighting=error +resharper_cpp_ue_version_file_doesnt_exist_highlighting=error +resharper_cpp_uninitialized_dependent_base_class_highlighting=warning +resharper_cpp_uninitialized_non_static_data_member_highlighting=warning +resharper_cpp_union_member_of_reference_type_highlighting=warning +resharper_cpp_unnamed_namespace_in_header_file_highlighting=warning +resharper_cpp_unnecessary_whitespace_highlighting=none +resharper_cpp_unreachable_code_highlighting=warning +resharper_cpp_unsigned_zero_comparison_highlighting=warning +resharper_cpp_unused_include_directive_highlighting=warning +resharper_cpp_user_defined_literal_suffix_does_not_start_with_underscore_highlighting=warning +resharper_cpp_use_algorithm_with_count_highlighting=suggestion +resharper_cpp_use_associative_contains_highlighting=suggestion +resharper_cpp_use_auto_for_numeric_highlighting=hint +resharper_cpp_use_auto_highlighting=hint +resharper_cpp_use_elements_view_highlighting=suggestion +resharper_cpp_use_erase_algorithm_highlighting=suggestion +resharper_cpp_use_familiar_template_syntax_for_generic_lambdas_highlighting=suggestion +resharper_cpp_use_range_algorithm_highlighting=suggestion +resharper_cpp_use_std_size_highlighting=suggestion +resharper_cpp_use_structured_binding_highlighting=hint +resharper_cpp_use_type_trait_alias_highlighting=suggestion +resharper_cpp_using_result_of_assignment_as_condition_highlighting=warning +resharper_cpp_u_function_macro_call_has_no_effect_highlighting=warning +resharper_cpp_u_property_macro_call_has_no_effect_highlighting=warning +resharper_cpp_variable_can_be_made_constexpr_highlighting=suggestion +resharper_cpp_virtual_function_call_inside_ctor_highlighting=warning +resharper_cpp_virtual_function_in_final_class_highlighting=warning +resharper_cpp_volatile_parameter_in_declaration_highlighting=suggestion +resharper_cpp_wrong_includes_order_highlighting=hint +resharper_cpp_wrong_indent_size_highlighting=none +resharper_cpp_wrong_slashes_in_include_directive_highlighting=hint +resharper_cpp_zero_constant_can_be_replaced_with_nullptr_highlighting=suggestion +resharper_cpp_zero_valued_expression_used_as_null_pointer_highlighting=warning +resharper_create_specialized_overload_highlighting=hint +resharper_css_browser_compatibility_highlighting=warning +resharper_css_caniuse_feature_requires_prefix_highlighting=hint +resharper_css_caniuse_unsupported_feature_highlighting=hint +resharper_css_not_resolved_highlighting=error +resharper_css_obsolete_highlighting=hint +resharper_css_property_does_not_override_vendor_property_highlighting=warning +resharper_cyclic_reference_comment_highlighting=none +resharper_c_declaration_with_implicit_int_type_highlighting=warning +resharper_c_sharp_build_cs_invalid_module_name_highlighting=warning +resharper_c_sharp_missing_plugin_dependency_highlighting=warning +resharper_declaration_hides_highlighting=hint +resharper_declaration_is_empty_highlighting=warning +resharper_declaration_visibility_error_highlighting=error +resharper_default_value_attribute_for_optional_parameter_highlighting=warning +resharper_deleting_non_qualified_reference_highlighting=error +resharper_dl_tag_contains_non_dt_or_dd_elements_highlighting=hint +resharper_double_colons_expected_highlighting=error +resharper_double_colons_preferred_highlighting=suggestion +resharper_double_negation_in_pattern_highlighting=suggestion +resharper_double_negation_of_boolean_highlighting=warning +resharper_double_negation_operator_highlighting=suggestion +resharper_duplicate_identifier_error_highlighting=error +resharper_duplicate_reference_comment_highlighting=warning +resharper_duplicate_resource_highlighting=warning +resharper_duplicating_local_declaration_highlighting=warning +resharper_duplicating_parameter_declaration_error_highlighting=error +resharper_duplicating_property_declaration_error_highlighting=error +resharper_duplicating_property_declaration_highlighting=warning +resharper_duplicating_switch_label_highlighting=warning +resharper_dynamic_shift_right_op_is_not_int_highlighting=warning +resharper_elided_trailing_element_highlighting=warning +resharper_empty_constructor_highlighting=warning +resharper_empty_destructor_highlighting=warning +resharper_empty_embedded_statement_highlighting=warning +resharper_empty_for_statement_highlighting=warning +resharper_empty_general_catch_clause_highlighting=warning +resharper_empty_namespace_highlighting=warning +resharper_empty_object_property_declaration_highlighting=error +resharper_empty_return_value_for_type_annotated_function_highlighting=warning +resharper_empty_statement_highlighting=warning +resharper_empty_title_tag_highlighting=hint +resharper_enforce_do_while_statement_braces_highlighting=none +resharper_enforce_fixed_statement_braces_highlighting=none +resharper_enforce_foreach_statement_braces_highlighting=none +resharper_enforce_for_statement_braces_highlighting=none +resharper_enforce_if_statement_braces_highlighting=none +resharper_enforce_lock_statement_braces_highlighting=none +resharper_enforce_using_statement_braces_highlighting=none +resharper_enforce_while_statement_braces_highlighting=none +resharper_entity_name_captured_only_global_highlighting=warning +resharper_entity_name_captured_only_local_highlighting=warning +resharper_enumerable_sum_in_explicit_unchecked_context_highlighting=warning +resharper_enum_underlying_type_is_int_highlighting=warning +resharper_equal_expression_comparison_highlighting=warning +resharper_error_in_xml_doc_reference_highlighting=error +resharper_es6_feature_highlighting=error +resharper_es7_feature_highlighting=error +resharper_eval_arguments_name_error_highlighting=error +resharper_event_never_invoked_global_highlighting=suggestion +resharper_event_never_subscribed_to_global_highlighting=suggestion +resharper_event_never_subscribed_to_local_highlighting=suggestion +resharper_event_unsubscription_via_anonymous_delegate_highlighting=warning +resharper_experimental_feature_highlighting=error +resharper_explicit_caller_info_argument_highlighting=warning +resharper_expression_is_always_const_highlighting=warning +resharper_expression_is_always_null_highlighting=warning +resharper_field_can_be_made_read_only_global_highlighting=suggestion +resharper_field_can_be_made_read_only_local_highlighting=suggestion +resharper_field_hides_interface_property_with_default_implementation_highlighting=warning +resharper_foreach_can_be_converted_to_query_using_another_get_enumerator_highlighting=hint +resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting=hint +resharper_format_string_placeholders_mismatch_highlighting=warning +resharper_format_string_problem_highlighting=warning +resharper_for_can_be_converted_to_foreach_highlighting=suggestion +resharper_for_statement_condition_is_true_highlighting=warning +resharper_functions_used_before_declared_highlighting=none +resharper_function_complexity_overflow_highlighting=none +resharper_function_never_returns_highlighting=warning +resharper_function_parameter_named_arguments_highlighting=warning +resharper_function_recursive_on_all_paths_highlighting=warning +resharper_function_used_out_of_scope_highlighting=warning +resharper_gc_suppress_finalize_for_type_without_destructor_highlighting=warning +resharper_generic_enumerator_not_disposed_highlighting=warning +resharper_heuristically_unreachable_code_highlighting=warning +resharper_heuristic_unreachable_code_highlighting=warning +resharper_hex_color_value_with_alpha_highlighting=error +resharper_html_attributes_quotes_highlighting=hint +resharper_html_attribute_not_resolved_highlighting=warning +resharper_html_attribute_value_not_resolved_highlighting=warning +resharper_html_dead_code_highlighting=warning +resharper_html_event_not_resolved_highlighting=warning +resharper_html_id_duplication_highlighting=warning +resharper_html_id_not_resolved_highlighting=warning +resharper_html_obsolete_highlighting=warning +resharper_html_path_error_highlighting=warning +resharper_html_tag_not_closed_highlighting=error +resharper_html_tag_not_resolved_highlighting=warning +resharper_html_tag_should_be_self_closed_highlighting=warning +resharper_html_tag_should_not_be_self_closed_highlighting=warning +resharper_html_warning_highlighting=warning +resharper_identifier_typo_highlighting=suggestion +resharper_implicit_any_error_highlighting=error +resharper_implicit_any_type_warning_highlighting=warning +resharper_import_keyword_not_with_invocation_highlighting=error +resharper_inactive_preprocessor_branch_highlighting=warning +resharper_inconsistently_synchronized_field_highlighting=warning +resharper_inconsistent_function_returns_highlighting=warning +resharper_inconsistent_naming_highlighting=warning +resharper_inconsistent_order_of_locks_highlighting=warning +resharper_incorrect_blank_lines_near_braces_highlighting=none +resharper_incorrect_operand_in_type_of_comparison_highlighting=warning +resharper_incorrect_triple_slash_location_highlighting=warning +resharper_indexing_by_invalid_range_highlighting=warning +resharper_inheritdoc_consider_usage_highlighting=none +resharper_inheritdoc_invalid_usage_highlighting=warning +resharper_inline_out_variable_declaration_highlighting=suggestion +resharper_inline_temporary_variable_highlighting=hint +resharper_internal_module_highlighting=suggestion +resharper_internal_or_private_member_not_documented_highlighting=none +resharper_interpolated_string_expression_is_not_i_formattable_highlighting=warning +resharper_introduce_optional_parameters_global_highlighting=suggestion +resharper_introduce_optional_parameters_local_highlighting=suggestion +resharper_introduce_variable_to_apply_guard_highlighting=hint +resharper_int_division_by_zero_highlighting=warning +resharper_int_variable_overflow_highlighting=warning +resharper_int_variable_overflow_in_checked_context_highlighting=warning +resharper_int_variable_overflow_in_unchecked_context_highlighting=warning +resharper_invalid_attribute_value_highlighting=warning +resharper_invalid_json_syntax_highlighting=error +resharper_invalid_task_element_highlighting=none +resharper_invalid_value_highlighting=error +resharper_invalid_value_type_highlighting=warning +resharper_invalid_xml_doc_comment_highlighting=warning +resharper_invert_condition_1_highlighting=hint +resharper_invert_if_highlighting=hint +resharper_invocation_is_skipped_highlighting=hint +resharper_invocation_of_non_function_highlighting=warning +resharper_invoked_expression_maybe_non_function_highlighting=warning +resharper_invoke_as_extension_method_highlighting=suggestion +resharper_is_expression_always_false_highlighting=warning +resharper_is_expression_always_true_highlighting=warning +resharper_iterator_method_result_is_ignored_highlighting=warning +resharper_iterator_never_returns_highlighting=warning +resharper_join_declaration_and_initializer_highlighting=suggestion +resharper_join_declaration_and_initializer_js_highlighting=suggestion +resharper_join_null_check_with_usage_highlighting=suggestion +resharper_join_null_check_with_usage_when_possible_highlighting=none +resharper_json_validation_failed_highlighting=error +resharper_js_path_not_found_highlighting=error +resharper_js_unreachable_code_highlighting=warning +resharper_jump_must_be_in_loop_highlighting=warning +resharper_label_or_semicolon_expected_highlighting=error +resharper_lambda_expression_can_be_made_static_highlighting=none +resharper_lambda_expression_must_be_static_highlighting=suggestion +resharper_lambda_highlighting=suggestion +resharper_lambda_should_not_capture_context_highlighting=warning +resharper_less_specific_overload_than_main_signature_highlighting=warning +resharper_lexical_declaration_needs_block_highlighting=error +resharper_localizable_element_highlighting=warning +resharper_local_function_can_be_made_static_highlighting=none +resharper_local_function_hides_method_highlighting=warning +resharper_local_function_redefined_later_highlighting=warning +resharper_local_variable_hides_member_highlighting=warning +resharper_long_literal_ending_lower_l_highlighting=warning +resharper_loop_can_be_converted_to_query_highlighting=hint +resharper_loop_can_be_partly_converted_to_query_highlighting=none +resharper_loop_variable_is_never_changed_inside_loop_highlighting=warning +resharper_l_value_is_expected_highlighting=error +resharper_markup_attribute_typo_highlighting=suggestion +resharper_markup_text_typo_highlighting=suggestion +resharper_math_abs_method_is_redundant_highlighting=warning +resharper_math_clamp_min_greater_than_max_highlighting=warning +resharper_meaningless_default_parameter_value_highlighting=warning +resharper_member_can_be_internal_highlighting=none +resharper_member_can_be_made_static_global_highlighting=hint +resharper_member_can_be_made_static_local_highlighting=hint +resharper_member_can_be_private_global_highlighting=suggestion +resharper_member_can_be_private_local_highlighting=suggestion +resharper_member_can_be_protected_global_highlighting=suggestion +resharper_member_can_be_protected_local_highlighting=suggestion +resharper_member_hides_interface_member_with_default_implementation_highlighting=warning +resharper_member_hides_static_from_outer_class_highlighting=warning +resharper_member_initializer_value_ignored_highlighting=warning +resharper_merge_and_pattern_highlighting=suggestion +resharper_merge_cast_with_type_check_highlighting=suggestion +resharper_merge_conditional_expression_highlighting=suggestion +resharper_merge_conditional_expression_when_possible_highlighting=none +resharper_merge_into_logical_pattern_highlighting=hint +resharper_merge_into_negated_pattern_highlighting=hint +resharper_merge_into_pattern_highlighting=suggestion +resharper_merge_nested_property_patterns_highlighting=suggestion +resharper_merge_sequential_checks_highlighting=hint +resharper_merge_sequential_checks_when_possible_highlighting=none +resharper_method_has_async_overload_highlighting=suggestion +resharper_method_has_async_overload_with_cancellation_highlighting=suggestion +resharper_method_overload_with_optional_parameter_highlighting=warning +resharper_method_safe_this_highlighting=suggestion +resharper_method_supports_cancellation_highlighting=suggestion +resharper_missing_alt_attribute_in_img_tag_highlighting=hint +resharper_missing_attribute_highlighting=warning +resharper_missing_blank_lines_highlighting=none +resharper_missing_body_tag_highlighting=warning +resharper_missing_has_own_property_in_foreach_highlighting=warning +resharper_missing_head_and_body_tags_highlighting=warning +resharper_missing_head_tag_highlighting=warning +resharper_missing_indent_highlighting=none +resharper_missing_linebreak_highlighting=none +resharper_missing_space_highlighting=none +resharper_missing_title_tag_highlighting=hint +resharper_misuse_of_owner_function_this_highlighting=warning +resharper_more_specific_foreach_variable_type_available_highlighting=suggestion +resharper_more_specific_signature_after_less_specific_highlighting=warning +resharper_move_to_existing_positional_deconstruction_pattern_highlighting=hint +resharper_multiple_declarations_in_foreach_highlighting=error +resharper_multiple_nullable_attributes_usage_highlighting=warning +resharper_multiple_order_by_highlighting=warning +resharper_multiple_output_tags_highlighting=warning +resharper_multiple_resolve_candidates_in_text_highlighting=warning +resharper_multiple_spaces_highlighting=none +resharper_multiple_statements_on_one_line_highlighting=none +resharper_multiple_type_members_on_one_line_highlighting=none +resharper_must_use_return_value_highlighting=warning +resharper_mvc_action_not_resolved_highlighting=error +resharper_mvc_area_not_resolved_highlighting=error +resharper_mvc_controller_not_resolved_highlighting=error +resharper_mvc_invalid_model_type_highlighting=error +resharper_mvc_masterpage_not_resolved_highlighting=error +resharper_mvc_partial_view_not_resolved_highlighting=error +resharper_mvc_template_not_resolved_highlighting=error +resharper_mvc_view_component_not_resolved_highlighting=error +resharper_mvc_view_component_view_not_resolved_highlighting=error +resharper_mvc_view_not_resolved_highlighting=error +resharper_native_type_prototype_extending_highlighting=warning +resharper_native_type_prototype_overwriting_highlighting=warning +resharper_negation_of_relational_pattern_highlighting=suggestion +resharper_negative_equality_expression_highlighting=suggestion +resharper_negative_index_highlighting=warning +resharper_nested_string_interpolation_highlighting=suggestion +resharper_non_assigned_constant_highlighting=error +resharper_non_atomic_compound_operator_highlighting=warning +resharper_non_constant_equality_expression_has_constant_result_highlighting=warning +resharper_non_parsable_element_highlighting=warning +resharper_non_readonly_member_in_get_hash_code_highlighting=warning +resharper_non_volatile_field_in_double_check_locking_highlighting=warning +resharper_not_accessed_field_global_highlighting=suggestion +resharper_not_accessed_field_local_highlighting=warning +resharper_not_accessed_positional_property_global_highlighting=warning +resharper_not_accessed_positional_property_local_highlighting=warning +resharper_not_accessed_variable_highlighting=warning +resharper_not_all_paths_return_value_highlighting=warning +resharper_not_assigned_out_parameter_highlighting=warning +resharper_not_declared_in_parent_culture_highlighting=warning +resharper_not_null_member_is_not_initialized_highlighting=warning +resharper_not_observable_annotation_redundancy_highlighting=warning +resharper_not_overridden_in_specific_culture_highlighting=warning +resharper_not_resolved_highlighting=warning +resharper_not_resolved_in_text_highlighting=warning +resharper_nullable_warning_suppression_is_used_highlighting=none +resharper_n_unit_async_method_must_be_task_highlighting=warning +resharper_n_unit_attribute_produces_too_many_tests_highlighting=none +resharper_n_unit_auto_fixture_incorrect_argument_type_highlighting=warning +resharper_n_unit_auto_fixture_missed_test_attribute_highlighting=warning +resharper_n_unit_auto_fixture_missed_test_or_test_fixture_attribute_highlighting=warning +resharper_n_unit_auto_fixture_redundant_argument_in_inline_auto_data_attribute_highlighting=warning +resharper_n_unit_duplicate_values_highlighting=warning +resharper_n_unit_ignored_parameter_attribute_highlighting=warning +resharper_n_unit_implicit_unspecified_null_values_highlighting=warning +resharper_n_unit_incorrect_argument_type_highlighting=warning +resharper_n_unit_incorrect_expected_result_type_highlighting=warning +resharper_n_unit_incorrect_range_bounds_highlighting=warning +resharper_n_unit_method_with_parameters_and_test_attribute_highlighting=warning +resharper_n_unit_missing_arguments_in_test_case_attribute_highlighting=warning +resharper_n_unit_non_public_method_with_test_attribute_highlighting=warning +resharper_n_unit_no_values_provided_highlighting=warning +resharper_n_unit_parameter_type_is_not_compatible_with_attribute_highlighting=warning +resharper_n_unit_range_attribute_bounds_are_out_of_range_highlighting=warning +resharper_n_unit_range_step_sign_mismatch_highlighting=warning +resharper_n_unit_range_step_value_must_not_be_zero_highlighting=warning +resharper_n_unit_range_to_value_is_not_reachable_highlighting=warning +resharper_n_unit_redundant_argument_instead_of_expected_result_highlighting=warning +resharper_n_unit_redundant_argument_in_test_case_attribute_highlighting=warning +resharper_n_unit_redundant_expected_result_in_test_case_attribute_highlighting=warning +resharper_n_unit_test_case_attribute_requires_expected_result_highlighting=warning +resharper_n_unit_test_case_result_property_duplicates_expected_result_highlighting=warning +resharper_n_unit_test_case_result_property_is_obsolete_highlighting=warning +resharper_n_unit_test_case_source_cannot_be_resolved_highlighting=warning +resharper_n_unit_test_case_source_must_be_field_property_method_highlighting=warning +resharper_n_unit_test_case_source_must_be_static_highlighting=warning +resharper_n_unit_test_case_source_should_implement_i_enumerable_highlighting=warning +resharper_object_creation_as_statement_highlighting=warning +resharper_object_destructuring_without_parentheses_highlighting=error +resharper_object_literals_are_not_comma_free_highlighting=error +resharper_obsolete_element_error_highlighting=error +resharper_obsolete_element_highlighting=warning +resharper_octal_literals_not_allowed_error_highlighting=error +resharper_ol_tag_contains_non_li_elements_highlighting=hint +resharper_one_way_operation_contract_with_return_type_highlighting=warning +resharper_operation_contract_without_service_contract_highlighting=warning +resharper_operator_is_can_be_used_highlighting=warning +resharper_optional_parameter_hierarchy_mismatch_highlighting=warning +resharper_optional_parameter_ref_out_highlighting=warning +resharper_other_tags_inside_script1_highlighting=error +resharper_other_tags_inside_script2_highlighting=error +resharper_other_tags_inside_unclosed_script_highlighting=error +resharper_outdent_is_off_prev_level_highlighting=none +resharper_output_tag_required_highlighting=warning +resharper_out_parameter_value_is_always_discarded_global_highlighting=suggestion +resharper_out_parameter_value_is_always_discarded_local_highlighting=warning +resharper_overload_signature_inferring_highlighting=hint +resharper_overridden_with_empty_value_highlighting=warning +resharper_overridden_with_same_value_highlighting=suggestion +resharper_parameter_doesnt_make_any_sense_highlighting=warning +resharper_parameter_hides_member_highlighting=warning +resharper_parameter_only_used_for_precondition_check_global_highlighting=suggestion +resharper_parameter_only_used_for_precondition_check_local_highlighting=warning +resharper_parameter_type_can_be_enumerable_global_highlighting=hint +resharper_parameter_type_can_be_enumerable_local_highlighting=hint +resharper_parameter_value_is_not_used_highlighting=warning +resharper_partial_method_parameter_name_mismatch_highlighting=warning +resharper_partial_method_with_single_part_highlighting=warning +resharper_partial_type_with_single_part_highlighting=warning +resharper_pass_string_interpolation_highlighting=hint +resharper_path_not_resolved_highlighting=error +resharper_pattern_always_matches_highlighting=warning +resharper_pattern_is_always_true_or_false_highlighting=warning +resharper_pattern_never_matches_highlighting=warning +resharper_polymorphic_field_like_event_invocation_highlighting=warning +resharper_possible_infinite_inheritance_highlighting=warning +resharper_possible_intended_rethrow_highlighting=warning +resharper_possible_interface_member_ambiguity_highlighting=warning +resharper_possible_invalid_cast_exception_highlighting=warning +resharper_possible_invalid_cast_exception_in_foreach_loop_highlighting=warning +resharper_possible_invalid_operation_exception_highlighting=warning +resharper_possible_loss_of_fraction_highlighting=warning +resharper_possible_mistaken_argument_highlighting=warning +resharper_possible_mistaken_call_to_get_type_1_highlighting=warning +resharper_possible_mistaken_call_to_get_type_2_highlighting=warning +resharper_possible_multiple_enumeration_highlighting=warning +resharper_possible_multiple_write_access_in_double_check_locking_highlighting=warning +resharper_possible_null_reference_exception_highlighting=warning +resharper_possible_struct_member_modification_of_non_variable_struct_highlighting=warning +resharper_possible_unintended_linear_search_in_set_highlighting=warning +resharper_possible_unintended_queryable_as_enumerable_highlighting=suggestion +resharper_possible_unintended_reference_comparison_highlighting=warning +resharper_possible_write_to_me_highlighting=warning +resharper_possibly_impure_method_call_on_readonly_variable_highlighting=warning +resharper_possibly_incorrectly_broken_statement_highlighting=warning +resharper_possibly_missing_indexer_initializer_comma_highlighting=warning +resharper_possibly_mistaken_use_of_interpolated_string_insert_highlighting=warning +resharper_possibly_mistaken_use_of_params_method_highlighting=warning +resharper_possibly_unassigned_property_highlighting=hint +resharper_private_field_can_be_converted_to_local_variable_highlighting=warning +resharper_private_variable_can_be_made_readonly_highlighting=hint +resharper_property_can_be_made_init_only_global_highlighting=suggestion +resharper_property_can_be_made_init_only_local_highlighting=suggestion +resharper_property_getter_cannot_have_parameters_highlighting=error +resharper_property_not_resolved_highlighting=error +resharper_property_setter_must_have_single_parameter_highlighting=error +resharper_public_constructor_in_abstract_class_highlighting=suggestion +resharper_pure_attribute_on_void_method_highlighting=warning +resharper_qualified_expression_is_null_highlighting=warning +resharper_qualified_expression_maybe_null_highlighting=warning +resharper_razor_layout_not_resolved_highlighting=error +resharper_razor_section_not_resolved_highlighting=error +resharper_read_access_in_double_check_locking_highlighting=warning +resharper_redundant_abstract_modifier_highlighting=warning +resharper_redundant_always_match_subpattern_highlighting=suggestion +resharper_redundant_anonymous_type_property_name_highlighting=warning +resharper_redundant_argument_default_value_highlighting=warning +resharper_redundant_array_creation_expression_highlighting=hint +resharper_redundant_array_lower_bound_specification_highlighting=warning +resharper_redundant_assignment_highlighting=warning +resharper_redundant_attribute_parentheses_highlighting=hint +resharper_redundant_attribute_usage_property_highlighting=suggestion +resharper_redundant_base_constructor_call_highlighting=warning +resharper_redundant_base_qualifier_highlighting=warning +resharper_redundant_blank_lines_highlighting=none +resharper_redundant_block_highlighting=warning +resharper_redundant_bool_compare_highlighting=warning +resharper_redundant_case_label_highlighting=warning +resharper_redundant_cast_highlighting=warning +resharper_redundant_catch_clause_highlighting=warning +resharper_redundant_check_before_assignment_highlighting=warning +resharper_redundant_collection_initializer_element_braces_highlighting=hint +resharper_redundant_comparison_with_boolean_highlighting=warning +resharper_redundant_configure_await_highlighting=suggestion +resharper_redundant_css_hack_highlighting=warning +resharper_redundant_declaration_semicolon_highlighting=hint +resharper_redundant_default_member_initializer_highlighting=warning +resharper_redundant_delegate_creation_highlighting=warning +resharper_redundant_disable_warning_comment_highlighting=warning +resharper_redundant_discard_designation_highlighting=suggestion +resharper_redundant_else_block_highlighting=warning +resharper_redundant_empty_case_else_highlighting=warning +resharper_redundant_empty_constructor_highlighting=warning +resharper_redundant_empty_finally_block_highlighting=warning +resharper_redundant_empty_object_creation_argument_list_highlighting=hint +resharper_redundant_empty_object_or_collection_initializer_highlighting=warning +resharper_redundant_empty_switch_section_highlighting=warning +resharper_redundant_enumerable_cast_call_highlighting=warning +resharper_redundant_enum_case_label_for_default_section_highlighting=none +resharper_redundant_explicit_array_creation_highlighting=warning +resharper_redundant_explicit_array_size_highlighting=warning +resharper_redundant_explicit_nullable_creation_highlighting=warning +resharper_redundant_explicit_params_array_creation_highlighting=suggestion +resharper_redundant_explicit_positional_property_declaration_highlighting=warning +resharper_redundant_explicit_tuple_component_name_highlighting=warning +resharper_redundant_extends_list_entry_highlighting=warning +resharper_redundant_fixed_pointer_declaration_highlighting=suggestion +resharper_redundant_highlighting=warning +resharper_redundant_if_else_block_highlighting=hint +resharper_redundant_if_statement_then_keyword_highlighting=none +resharper_redundant_immediate_delegate_invocation_highlighting=suggestion +resharper_redundant_intermediate_variable_highlighting=hint +resharper_redundant_is_before_relational_pattern_highlighting=suggestion +resharper_redundant_iterator_keyword_highlighting=warning +resharper_redundant_jump_statement_highlighting=warning +resharper_redundant_lambda_parameter_type_highlighting=warning +resharper_redundant_lambda_signature_parentheses_highlighting=hint +resharper_redundant_linebreak_highlighting=none +resharper_redundant_local_class_name_highlighting=hint +resharper_redundant_local_function_name_highlighting=hint +resharper_redundant_logical_conditional_expression_operand_highlighting=warning +resharper_redundant_me_qualifier_highlighting=warning +resharper_redundant_my_base_qualifier_highlighting=warning +resharper_redundant_my_class_qualifier_highlighting=warning +resharper_redundant_name_qualifier_highlighting=warning +resharper_redundant_not_null_constraint_highlighting=warning +resharper_redundant_nullable_annotation_on_reference_type_constraint_highlighting=warning +resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_base_type_highlighting=warning +resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_type_kind_highlighting=warning +resharper_redundant_nullable_flow_attribute_highlighting=warning +resharper_redundant_nullable_type_mark_highlighting=warning +resharper_redundant_nullness_attribute_with_nullable_reference_types_highlighting=warning +resharper_redundant_overflow_checking_context_highlighting=warning +resharper_redundant_overload_global_highlighting=suggestion +resharper_redundant_overload_local_highlighting=suggestion +resharper_redundant_overridden_member_highlighting=warning +resharper_redundant_params_highlighting=warning +resharper_redundant_parentheses_highlighting=none +resharper_redundant_parent_type_declaration_highlighting=warning +resharper_redundant_pattern_parentheses_highlighting=hint +resharper_redundant_property_parentheses_highlighting=hint +resharper_redundant_property_pattern_clause_highlighting=suggestion +resharper_redundant_qualifier_highlighting=warning +resharper_redundant_query_order_by_ascending_keyword_highlighting=hint +resharper_redundant_range_bound_highlighting=suggestion +resharper_redundant_readonly_modifier_highlighting=suggestion +resharper_redundant_record_body_highlighting=warning +resharper_redundant_record_class_keyword_highlighting=warning +resharper_redundant_setter_value_parameter_declaration_highlighting=hint +resharper_redundant_space_highlighting=none +resharper_redundant_string_format_call_highlighting=warning +resharper_redundant_string_interpolation_highlighting=suggestion +resharper_redundant_string_to_char_array_call_highlighting=warning +resharper_redundant_string_type_highlighting=suggestion +resharper_redundant_suppress_nullable_warning_expression_highlighting=warning +resharper_redundant_ternary_expression_highlighting=warning +resharper_redundant_to_string_call_for_value_type_highlighting=hint +resharper_redundant_to_string_call_highlighting=warning +resharper_redundant_type_arguments_of_method_highlighting=warning +resharper_redundant_type_cast_highlighting=warning +resharper_redundant_type_cast_structural_highlighting=warning +resharper_redundant_type_check_in_pattern_highlighting=warning +resharper_redundant_units_highlighting=warning +resharper_redundant_unsafe_context_highlighting=warning +resharper_redundant_using_directive_global_highlighting=warning +resharper_redundant_using_directive_highlighting=warning +resharper_redundant_variable_type_specification_highlighting=hint +resharper_redundant_verbatim_prefix_highlighting=suggestion +resharper_redundant_verbatim_string_prefix_highlighting=suggestion +resharper_redundant_with_expression_highlighting=suggestion +resharper_reference_equals_with_value_type_highlighting=warning +resharper_reg_exp_inspections_highlighting=warning +resharper_remove_constructor_invocation_highlighting=none +resharper_remove_redundant_braces_highlighting=none +resharper_remove_redundant_or_statement_false_highlighting=suggestion +resharper_remove_redundant_or_statement_true_highlighting=suggestion +resharper_remove_to_list_1_highlighting=suggestion +resharper_remove_to_list_2_highlighting=suggestion +resharper_replace_auto_property_with_computed_property_highlighting=hint +resharper_replace_indicing_with_array_destructuring_highlighting=hint +resharper_replace_indicing_with_short_hand_properties_after_destructuring_highlighting=hint +resharper_replace_object_pattern_with_var_pattern_highlighting=suggestion +resharper_replace_slice_with_range_indexer_highlighting=hint +resharper_replace_substring_with_range_indexer_highlighting=hint +resharper_replace_undefined_checking_series_with_object_destructuring_highlighting=hint +resharper_replace_with_destructuring_swap_highlighting=hint +resharper_replace_with_first_or_default_1_highlighting=suggestion +resharper_replace_with_first_or_default_2_highlighting=suggestion +resharper_replace_with_first_or_default_3_highlighting=suggestion +resharper_replace_with_first_or_default_4_highlighting=suggestion +resharper_replace_with_last_or_default_1_highlighting=suggestion +resharper_replace_with_last_or_default_2_highlighting=suggestion +resharper_replace_with_last_or_default_3_highlighting=suggestion +resharper_replace_with_last_or_default_4_highlighting=suggestion +resharper_replace_with_of_type_1_highlighting=suggestion +resharper_replace_with_of_type_2_highlighting=suggestion +resharper_replace_with_of_type_3_highlighting=suggestion +resharper_replace_with_of_type_any_1_highlighting=suggestion +resharper_replace_with_of_type_any_2_highlighting=suggestion +resharper_replace_with_of_type_count_1_highlighting=suggestion +resharper_replace_with_of_type_count_2_highlighting=suggestion +resharper_replace_with_of_type_first_1_highlighting=suggestion +resharper_replace_with_of_type_first_2_highlighting=suggestion +resharper_replace_with_of_type_first_or_default_1_highlighting=suggestion +resharper_replace_with_of_type_first_or_default_2_highlighting=suggestion +resharper_replace_with_of_type_last_1_highlighting=suggestion +resharper_replace_with_of_type_last_2_highlighting=suggestion +resharper_replace_with_of_type_last_or_default_1_highlighting=suggestion +resharper_replace_with_of_type_last_or_default_2_highlighting=suggestion +resharper_replace_with_of_type_long_count_highlighting=suggestion +resharper_replace_with_of_type_single_1_highlighting=suggestion +resharper_replace_with_of_type_single_2_highlighting=suggestion +resharper_replace_with_of_type_single_or_default_1_highlighting=suggestion +resharper_replace_with_of_type_single_or_default_2_highlighting=suggestion +resharper_replace_with_of_type_where_highlighting=suggestion +resharper_replace_with_simple_assignment_false_highlighting=suggestion +resharper_replace_with_simple_assignment_true_highlighting=suggestion +resharper_replace_with_single_assignment_false_highlighting=suggestion +resharper_replace_with_single_assignment_true_highlighting=suggestion +resharper_replace_with_single_call_to_any_highlighting=suggestion +resharper_replace_with_single_call_to_count_highlighting=suggestion +resharper_replace_with_single_call_to_first_highlighting=suggestion +resharper_replace_with_single_call_to_first_or_default_highlighting=suggestion +resharper_replace_with_single_call_to_last_highlighting=suggestion +resharper_replace_with_single_call_to_last_or_default_highlighting=suggestion +resharper_replace_with_single_call_to_single_highlighting=suggestion +resharper_replace_with_single_call_to_single_or_default_highlighting=suggestion +resharper_replace_with_single_or_default_1_highlighting=suggestion +resharper_replace_with_single_or_default_2_highlighting=suggestion +resharper_replace_with_single_or_default_3_highlighting=suggestion +resharper_replace_with_single_or_default_4_highlighting=suggestion +resharper_replace_with_string_is_null_or_empty_highlighting=suggestion +resharper_required_base_types_conflict_highlighting=warning +resharper_required_base_types_direct_conflict_highlighting=warning +resharper_required_base_types_is_not_inherited_highlighting=warning +resharper_requires_fallback_color_highlighting=warning +resharper_resource_item_not_resolved_highlighting=error +resharper_resource_not_resolved_highlighting=error +resharper_resx_not_resolved_highlighting=warning +resharper_return_from_global_scopet_with_value_highlighting=warning +resharper_return_type_can_be_enumerable_global_highlighting=hint +resharper_return_type_can_be_enumerable_local_highlighting=hint +resharper_return_type_can_be_not_nullable_highlighting=warning +resharper_return_value_of_pure_method_is_not_used_highlighting=warning +resharper_route_templates_action_route_prefix_can_be_extracted_to_controller_route_highlighting=hint +resharper_route_templates_ambiguous_matching_constraint_constructor_highlighting=warning +resharper_route_templates_ambiguous_route_match_highlighting=warning +resharper_route_templates_constraint_argument_cannot_be_converted_highlighting=warning +resharper_route_templates_controller_route_parameter_is_not_passed_to_methods_highlighting=hint +resharper_route_templates_duplicated_parameter_highlighting=warning +resharper_route_templates_matching_constraint_constructor_not_resolved_highlighting=warning +resharper_route_templates_method_missing_route_parameters_highlighting=hint +resharper_route_templates_optional_parameter_can_be_preceded_only_by_single_period_highlighting=warning +resharper_route_templates_optional_parameter_must_be_at_the_end_of_segment_highlighting=warning +resharper_route_templates_parameter_constraint_can_be_specified_highlighting=hint +resharper_route_templates_parameter_type_and_constraints_mismatch_highlighting=warning +resharper_route_templates_parameter_type_can_be_made_stricter_highlighting=suggestion +resharper_route_templates_route_parameter_constraint_not_resolved_highlighting=warning +resharper_route_templates_route_parameter_is_not_passed_to_method_highlighting=hint +resharper_route_templates_route_token_not_resolved_highlighting=warning +resharper_route_templates_symbol_not_resolved_highlighting=warning +resharper_route_templates_syntax_error_highlighting=warning +resharper_safe_cast_is_used_as_type_check_highlighting=suggestion +resharper_same_imports_with_different_name_highlighting=warning +resharper_same_variable_assignment_highlighting=warning +resharper_script_tag_has_both_src_and_content_attributes_highlighting=error +resharper_script_tag_with_content_before_includes_highlighting=hint +resharper_sealed_member_in_sealed_class_highlighting=warning +resharper_separate_control_transfer_statement_highlighting=none +resharper_service_contract_without_operations_highlighting=warning +resharper_shift_expression_real_shift_count_is_zero_highlighting=warning +resharper_shift_expression_result_equals_zero_highlighting=warning +resharper_shift_expression_right_operand_not_equal_real_count_highlighting=warning +resharper_shift_expression_zero_left_operand_highlighting=warning +resharper_similar_anonymous_type_nearby_highlighting=hint +resharper_similar_expressions_comparison_highlighting=warning +resharper_simplify_conditional_operator_highlighting=suggestion +resharper_simplify_conditional_ternary_expression_highlighting=suggestion +resharper_simplify_i_if_highlighting=suggestion +resharper_simplify_linq_expression_use_all_highlighting=suggestion +resharper_simplify_linq_expression_use_any_highlighting=suggestion +resharper_simplify_string_interpolation_highlighting=suggestion +resharper_specify_a_culture_in_string_conversion_explicitly_highlighting=warning +resharper_specify_string_comparison_highlighting=hint +resharper_specify_variable_type_explicitly_highlighting=hint +resharper_spin_lock_in_readonly_field_highlighting=warning +resharper_stack_alloc_inside_loop_highlighting=warning +resharper_statement_termination_highlighting=warning +resharper_static_member_initializer_referes_to_member_below_highlighting=warning +resharper_static_member_in_generic_type_highlighting=none +resharper_static_problem_in_text_highlighting=warning +resharper_string_compare_is_culture_specific_1_highlighting=warning +resharper_string_compare_is_culture_specific_2_highlighting=warning +resharper_string_compare_is_culture_specific_3_highlighting=warning +resharper_string_compare_is_culture_specific_4_highlighting=warning +resharper_string_compare_is_culture_specific_5_highlighting=warning +resharper_string_compare_is_culture_specific_6_highlighting=warning +resharper_string_compare_to_is_culture_specific_highlighting=warning +resharper_string_concatenation_to_template_string_highlighting=hint +resharper_string_ends_with_is_culture_specific_highlighting=none +resharper_string_index_of_is_culture_specific_1_highlighting=warning +resharper_string_index_of_is_culture_specific_2_highlighting=warning +resharper_string_index_of_is_culture_specific_3_highlighting=warning +resharper_string_last_index_of_is_culture_specific_1_highlighting=warning +resharper_string_last_index_of_is_culture_specific_2_highlighting=warning +resharper_string_last_index_of_is_culture_specific_3_highlighting=warning +resharper_string_literal_as_interpolation_argument_highlighting=suggestion +resharper_string_literal_typo_highlighting=suggestion +resharper_string_literal_wrong_quotes_highlighting=hint +resharper_string_starts_with_is_culture_specific_highlighting=none +resharper_structured_message_template_problem_highlighting=warning +resharper_struct_can_be_made_read_only_highlighting=suggestion +resharper_struct_member_can_be_made_read_only_highlighting=none +resharper_suggest_base_type_for_parameter_highlighting=hint +resharper_suggest_base_type_for_parameter_in_constructor_highlighting=hint +resharper_suggest_discard_declaration_var_style_highlighting=hint +resharper_suggest_var_or_type_built_in_types_highlighting=hint +resharper_suggest_var_or_type_deconstruction_declarations_highlighting=hint +resharper_suggest_var_or_type_elsewhere_highlighting=hint +resharper_suggest_var_or_type_simple_types_highlighting=hint +resharper_super_call_highlighting=suggestion +resharper_super_call_prohibits_this_highlighting=error +resharper_suppress_nullable_warning_expression_as_inverted_is_expression_highlighting=warning +resharper_suspicious_instanceof_check_highlighting=warning +resharper_suspicious_lambda_block_highlighting=warning +resharper_suspicious_lock_over_synchronization_primitive_highlighting=warning +resharper_suspicious_math_sign_method_highlighting=warning +resharper_suspicious_parameter_name_in_argument_null_exception_highlighting=warning +resharper_suspicious_this_usage_highlighting=warning +resharper_suspicious_typeof_check_highlighting=warning +resharper_suspicious_type_conversion_global_highlighting=warning +resharper_swap_via_deconstruction_highlighting=suggestion +resharper_switch_expression_handles_some_known_enum_values_with_exception_in_default_highlighting=hint +resharper_switch_statement_for_enum_misses_default_section_highlighting=hint +resharper_switch_statement_handles_some_known_enum_values_with_default_highlighting=hint +resharper_switch_statement_missing_some_enum_cases_no_default_highlighting=none +resharper_symbol_from_not_copied_locally_reference_used_warning_highlighting=warning +resharper_syntax_is_not_allowed_highlighting=warning +resharper_tabs_and_spaces_mismatch_highlighting=none +resharper_tabs_are_disallowed_highlighting=none +resharper_tabs_outside_indent_highlighting=none +resharper_tail_recursive_call_highlighting=hint +resharper_tasks_not_loaded_highlighting=warning +resharper_ternary_can_be_replaced_by_its_condition_highlighting=warning +resharper_this_in_global_context_highlighting=warning +resharper_thread_static_at_instance_field_highlighting=warning +resharper_thread_static_field_has_initializer_highlighting=warning +resharper_throw_must_be_followed_by_expression_highlighting=error +resharper_too_wide_local_variable_scope_highlighting=suggestion +resharper_try_cast_always_succeeds_highlighting=suggestion +resharper_try_statements_can_be_merged_highlighting=hint +resharper_ts_not_resolved_highlighting=error +resharper_ts_resolved_from_inaccessible_module_highlighting=error +resharper_type_guard_doesnt_affect_anything_highlighting=warning +resharper_type_guard_produces_never_type_highlighting=warning +resharper_type_parameter_can_be_variant_highlighting=suggestion +resharper_type_parameter_hides_type_param_from_outer_scope_highlighting=warning +resharper_ul_tag_contains_non_li_elements_highlighting=hint +resharper_unassigned_field_global_highlighting=suggestion +resharper_unassigned_field_local_highlighting=warning +resharper_unassigned_get_only_auto_property_highlighting=warning +resharper_unassigned_readonly_field_highlighting=warning +resharper_unclosed_script_highlighting=error +resharper_undeclared_global_variable_using_highlighting=warning +resharper_unexpected_value_highlighting=error +resharper_unknown_css_class_highlighting=warning +resharper_unknown_css_variable_highlighting=warning +resharper_unknown_css_vendor_extension_highlighting=hint +resharper_unknown_item_group_highlighting=warning +resharper_unknown_metadata_highlighting=warning +resharper_unknown_output_parameter_highlighting=warning +resharper_unknown_property_highlighting=warning +resharper_unknown_target_highlighting=warning +resharper_unknown_task_attribute_highlighting=warning +resharper_unknown_task_highlighting=warning +resharper_unnecessary_whitespace_highlighting=none +resharper_unreachable_switch_arm_due_to_integer_analysis_highlighting=warning +resharper_unreachable_switch_case_due_to_integer_analysis_highlighting=warning +resharper_unreal_header_tool_error_highlighting=error +resharper_unreal_header_tool_parser_error_highlighting=error +resharper_unreal_header_tool_warning_highlighting=warning +resharper_unsafe_comma_in_object_properties_list_highlighting=warning +resharper_unsupported_required_base_type_highlighting=warning +resharper_unused_anonymous_method_signature_highlighting=warning +resharper_unused_auto_property_accessor_global_highlighting=warning +resharper_unused_auto_property_accessor_local_highlighting=warning +resharper_unused_import_clause_highlighting=warning +resharper_unused_inherited_parameter_highlighting=hint +resharper_unused_locals_highlighting=warning +resharper_unused_local_function_highlighting=warning +resharper_unused_local_function_parameter_highlighting=warning +resharper_unused_local_function_return_value_highlighting=warning +resharper_unused_local_import_highlighting=warning +resharper_unused_member_global_highlighting=suggestion +resharper_unused_member_hierarchy_global_highlighting=suggestion +resharper_unused_member_hierarchy_local_highlighting=warning +resharper_unused_member_in_super_global_highlighting=suggestion +resharper_unused_member_in_super_local_highlighting=warning +resharper_unused_member_local_highlighting=warning +resharper_unused_method_return_value_global_highlighting=suggestion +resharper_unused_method_return_value_local_highlighting=warning +resharper_unused_parameter_global_highlighting=suggestion +resharper_unused_parameter_highlighting=warning +resharper_unused_parameter_in_partial_method_highlighting=warning +resharper_unused_parameter_local_highlighting=warning +resharper_unused_property_highlighting=warning +resharper_unused_tuple_component_in_return_value_highlighting=warning +resharper_unused_type_global_highlighting=suggestion +resharper_unused_type_local_highlighting=warning +resharper_unused_type_parameter_highlighting=warning +resharper_unused_variable_highlighting=warning +resharper_usage_of_definitely_unassigned_value_highlighting=warning +resharper_usage_of_possibly_unassigned_value_highlighting=warning +resharper_useless_binary_operation_highlighting=warning +resharper_useless_comparison_to_integral_constant_highlighting=warning +resharper_use_array_creation_expression_1_highlighting=suggestion +resharper_use_array_creation_expression_2_highlighting=suggestion +resharper_use_array_empty_method_highlighting=suggestion +resharper_use_as_instead_of_type_cast_highlighting=hint +resharper_use_await_using_highlighting=suggestion +resharper_use_cancellation_token_for_i_async_enumerable_highlighting=suggestion +resharper_use_collection_count_property_highlighting=suggestion +resharper_use_configure_await_false_for_async_disposable_highlighting=none +resharper_use_configure_await_false_highlighting=suggestion +resharper_use_deconstruction_highlighting=hint +resharper_use_deconstruction_on_parameter_highlighting=hint +resharper_use_empty_types_field_highlighting=suggestion +resharper_use_event_args_empty_field_highlighting=suggestion +resharper_use_format_specifier_in_format_string_highlighting=suggestion +resharper_use_implicitly_typed_variable_evident_highlighting=hint +resharper_use_implicitly_typed_variable_highlighting=none +resharper_use_implicit_by_val_modifier_highlighting=hint +resharper_use_indexed_property_highlighting=suggestion +resharper_use_index_from_end_expression_highlighting=suggestion +resharper_use_is_operator_1_highlighting=suggestion +resharper_use_is_operator_2_highlighting=suggestion +resharper_use_method_any_0_highlighting=suggestion +resharper_use_method_any_1_highlighting=suggestion +resharper_use_method_any_2_highlighting=suggestion +resharper_use_method_any_3_highlighting=suggestion +resharper_use_method_any_4_highlighting=suggestion +resharper_use_method_is_instance_of_type_highlighting=suggestion +resharper_use_nameof_expression_for_part_of_the_string_highlighting=none +resharper_use_nameof_expression_highlighting=suggestion +resharper_use_name_of_instead_of_type_of_highlighting=suggestion +resharper_use_negated_pattern_in_is_expression_highlighting=hint +resharper_use_negated_pattern_matching_highlighting=hint +resharper_use_nullable_annotation_instead_of_attribute_highlighting=suggestion +resharper_use_nullable_attributes_supported_by_compiler_highlighting=suggestion +resharper_use_nullable_reference_types_annotation_syntax_highlighting=warning +resharper_use_null_propagation_highlighting=hint +resharper_use_null_propagation_when_possible_highlighting=none +resharper_use_object_or_collection_initializer_highlighting=suggestion +resharper_use_of_implicit_global_in_function_scope_highlighting=warning +resharper_use_of_possibly_unassigned_property_highlighting=warning +resharper_use_pattern_matching_highlighting=suggestion +resharper_use_positional_deconstruction_pattern_highlighting=none +resharper_use_string_interpolation_highlighting=suggestion +resharper_use_switch_case_pattern_variable_highlighting=suggestion +resharper_use_throw_if_null_method_highlighting=none +resharper_use_verbatim_string_highlighting=hint +resharper_using_of_reserved_word_error_highlighting=error +resharper_using_of_reserved_word_highlighting=warning +resharper_value_parameter_not_used_highlighting=warning +resharper_value_range_attribute_violation_highlighting=warning +resharper_value_should_have_units_highlighting=error +resharper_variable_can_be_made_const_highlighting=hint +resharper_variable_can_be_made_let_highlighting=hint +resharper_variable_can_be_moved_to_inner_block_highlighting=hint +resharper_variable_can_be_not_nullable_highlighting=warning +resharper_variable_hides_outer_variable_highlighting=warning +resharper_variable_used_before_declared_highlighting=warning +resharper_variable_used_in_inner_scope_before_declared_highlighting=warning +resharper_variable_used_out_of_scope_highlighting=warning +resharper_vb_check_for_reference_equality_instead_1_highlighting=suggestion +resharper_vb_check_for_reference_equality_instead_2_highlighting=suggestion +resharper_vb_possible_mistaken_argument_highlighting=warning +resharper_vb_possible_mistaken_call_to_get_type_1_highlighting=warning +resharper_vb_possible_mistaken_call_to_get_type_2_highlighting=warning +resharper_vb_remove_to_list_1_highlighting=suggestion +resharper_vb_remove_to_list_2_highlighting=suggestion +resharper_vb_replace_with_first_or_default_highlighting=suggestion +resharper_vb_replace_with_last_or_default_highlighting=suggestion +resharper_vb_replace_with_of_type_1_highlighting=suggestion +resharper_vb_replace_with_of_type_2_highlighting=suggestion +resharper_vb_replace_with_of_type_any_1_highlighting=suggestion +resharper_vb_replace_with_of_type_any_2_highlighting=suggestion +resharper_vb_replace_with_of_type_count_1_highlighting=suggestion +resharper_vb_replace_with_of_type_count_2_highlighting=suggestion +resharper_vb_replace_with_of_type_first_1_highlighting=suggestion +resharper_vb_replace_with_of_type_first_2_highlighting=suggestion +resharper_vb_replace_with_of_type_first_or_default_1_highlighting=suggestion +resharper_vb_replace_with_of_type_first_or_default_2_highlighting=suggestion +resharper_vb_replace_with_of_type_last_1_highlighting=suggestion +resharper_vb_replace_with_of_type_last_2_highlighting=suggestion +resharper_vb_replace_with_of_type_last_or_default_1_highlighting=suggestion +resharper_vb_replace_with_of_type_last_or_default_2_highlighting=suggestion +resharper_vb_replace_with_of_type_single_1_highlighting=suggestion +resharper_vb_replace_with_of_type_single_2_highlighting=suggestion +resharper_vb_replace_with_of_type_single_or_default_1_highlighting=suggestion +resharper_vb_replace_with_of_type_single_or_default_2_highlighting=suggestion +resharper_vb_replace_with_of_type_where_highlighting=suggestion +resharper_vb_replace_with_single_assignment_1_highlighting=suggestion +resharper_vb_replace_with_single_assignment_2_highlighting=suggestion +resharper_vb_replace_with_single_call_to_any_highlighting=suggestion +resharper_vb_replace_with_single_call_to_count_highlighting=suggestion +resharper_vb_replace_with_single_call_to_first_highlighting=suggestion +resharper_vb_replace_with_single_call_to_first_or_default_highlighting=suggestion +resharper_vb_replace_with_single_call_to_last_highlighting=suggestion +resharper_vb_replace_with_single_call_to_last_or_default_highlighting=suggestion +resharper_vb_replace_with_single_call_to_single_highlighting=suggestion +resharper_vb_replace_with_single_call_to_single_or_default_highlighting=suggestion +resharper_vb_replace_with_single_or_default_highlighting=suggestion +resharper_vb_simplify_linq_expression_10_highlighting=hint +resharper_vb_simplify_linq_expression_1_highlighting=suggestion +resharper_vb_simplify_linq_expression_2_highlighting=suggestion +resharper_vb_simplify_linq_expression_3_highlighting=suggestion +resharper_vb_simplify_linq_expression_4_highlighting=suggestion +resharper_vb_simplify_linq_expression_5_highlighting=suggestion +resharper_vb_simplify_linq_expression_6_highlighting=suggestion +resharper_vb_simplify_linq_expression_7_highlighting=hint +resharper_vb_simplify_linq_expression_8_highlighting=hint +resharper_vb_simplify_linq_expression_9_highlighting=hint +resharper_vb_string_compare_is_culture_specific_1_highlighting=warning +resharper_vb_string_compare_is_culture_specific_2_highlighting=warning +resharper_vb_string_compare_is_culture_specific_3_highlighting=warning +resharper_vb_string_compare_is_culture_specific_4_highlighting=warning +resharper_vb_string_compare_is_culture_specific_5_highlighting=warning +resharper_vb_string_compare_is_culture_specific_6_highlighting=warning +resharper_vb_string_compare_to_is_culture_specific_highlighting=warning +resharper_vb_string_ends_with_is_culture_specific_highlighting=none +resharper_vb_string_index_of_is_culture_specific_1_highlighting=warning +resharper_vb_string_index_of_is_culture_specific_2_highlighting=warning +resharper_vb_string_index_of_is_culture_specific_3_highlighting=warning +resharper_vb_string_last_index_of_is_culture_specific_1_highlighting=warning +resharper_vb_string_last_index_of_is_culture_specific_2_highlighting=warning +resharper_vb_string_last_index_of_is_culture_specific_3_highlighting=warning +resharper_vb_string_starts_with_is_culture_specific_highlighting=none +resharper_vb_unreachable_code_highlighting=warning +resharper_vb_use_array_creation_expression_1_highlighting=suggestion +resharper_vb_use_array_creation_expression_2_highlighting=suggestion +resharper_vb_use_first_instead_highlighting=warning +resharper_vb_use_method_any_1_highlighting=suggestion +resharper_vb_use_method_any_2_highlighting=suggestion +resharper_vb_use_method_any_3_highlighting=suggestion +resharper_vb_use_method_any_4_highlighting=suggestion +resharper_vb_use_method_any_5_highlighting=suggestion +resharper_vb_use_method_is_instance_of_type_highlighting=suggestion +resharper_vb_use_type_of_is_operator_1_highlighting=suggestion +resharper_vb_use_type_of_is_operator_2_highlighting=suggestion +resharper_virtual_member_call_in_constructor_highlighting=warning +resharper_virtual_member_never_overridden_global_highlighting=suggestion +resharper_virtual_member_never_overridden_local_highlighting=suggestion +resharper_void_method_with_must_use_return_value_attribute_highlighting=warning +resharper_web_config_module_not_resolved_highlighting=error +resharper_web_config_module_qualification_resolve_highlighting=warning +resharper_web_config_redundant_add_namespace_tag_highlighting=warning +resharper_web_config_redundant_location_tag_highlighting=warning +resharper_web_config_tag_prefix_redundand_highlighting=warning +resharper_web_config_type_not_resolved_highlighting=error +resharper_web_config_unused_add_tag_highlighting=warning +resharper_web_config_unused_element_due_to_config_source_attribute_highlighting=warning +resharper_web_config_unused_remove_or_clear_tag_highlighting=warning +resharper_web_config_web_config_path_warning_highlighting=warning +resharper_web_config_wrong_module_highlighting=error +resharper_web_ignored_path_highlighting=none +resharper_web_mapped_path_highlighting=hint +resharper_with_expression_instead_of_initializer_highlighting=suggestion +resharper_with_statement_using_error_highlighting=error +resharper_wrong_expression_statement_highlighting=warning +resharper_wrong_indent_size_highlighting=none +resharper_wrong_metadata_use_highlighting=none +resharper_wrong_public_modifier_specification_highlighting=hint +resharper_wrong_require_relative_path_highlighting=hint +resharper_xaml_assign_null_to_not_null_attribute_highlighting=warning +resharper_xaml_avalonia_wrong_binding_mode_for_stream_binding_operator_highlighting=warning +resharper_xaml_binding_without_context_not_resolved_highlighting=hint +resharper_xaml_binding_with_context_not_resolved_highlighting=warning +resharper_xaml_compiled_binding_missing_data_type_error_highlighting_highlighting=error +resharper_xaml_constructor_warning_highlighting=warning +resharper_xaml_decimal_parsing_is_culture_dependent_highlighting=warning +resharper_xaml_dependency_property_resolve_error_highlighting=warning +resharper_xaml_duplicate_style_setter_highlighting=warning +resharper_xaml_dynamic_resource_error_highlighting=error +resharper_xaml_element_name_reference_not_resolved_highlighting=error +resharper_xaml_empty_grid_length_definition_highlighting=error +resharper_xaml_grid_definitions_can_be_converted_to_attribute_highlighting=hint +resharper_xaml_ignored_path_highlighting_highlighting=none +resharper_xaml_index_out_of_grid_definition_highlighting=warning +resharper_xaml_invalid_member_type_highlighting=error +resharper_xaml_invalid_resource_target_type_highlighting=error +resharper_xaml_invalid_resource_type_highlighting=error +resharper_xaml_invalid_type_highlighting=error +resharper_xaml_language_level_highlighting=error +resharper_xaml_mapped_path_highlighting_highlighting=hint +resharper_xaml_method_arguments_will_be_ignored_highlighting=warning +resharper_xaml_missing_grid_index_highlighting=warning +resharper_xaml_overloads_collision_highlighting=warning +resharper_xaml_parent_is_out_of_current_component_tree_highlighting=warning +resharper_xaml_path_error_highlighting=warning +resharper_xaml_possible_null_reference_exception_highlighting=suggestion +resharper_xaml_redundant_attached_property_highlighting=warning +resharper_xaml_redundant_binding_mode_attribute_highlighting=warning +resharper_xaml_redundant_collection_property_highlighting=warning +resharper_xaml_redundant_freeze_attribute_highlighting=warning +resharper_xaml_redundant_grid_definitions_highlighting=warning +resharper_xaml_redundant_grid_span_highlighting=warning +resharper_xaml_redundant_modifiers_attribute_highlighting=warning +resharper_xaml_redundant_namespace_alias_highlighting=warning +resharper_xaml_redundant_name_attribute_highlighting=warning +resharper_xaml_redundant_property_type_qualifier_highlighting=warning +resharper_xaml_redundant_resource_highlighting=warning +resharper_xaml_redundant_styled_value_highlighting=warning +resharper_xaml_redundant_update_source_trigger_attribute_highlighting=warning +resharper_xaml_redundant_xamarin_forms_class_declaration_highlighting=warning +resharper_xaml_resource_file_path_case_mismatch_highlighting=warning +resharper_xaml_routed_event_resolve_error_highlighting=warning +resharper_xaml_static_resource_not_resolved_highlighting=warning +resharper_xaml_style_class_not_found_highlighting=warning +resharper_xaml_style_invalid_target_type_highlighting=error +resharper_xaml_unexpected_text_token_highlighting=error +resharper_xaml_xaml_duplicate_device_family_type_view_highlighting_highlighting=error +resharper_xaml_xaml_mismatched_device_family_view_clr_name_highlighting_highlighting=warning +resharper_xaml_xaml_relative_source_default_mode_warning_highlighting_highlighting=warning +resharper_xaml_xaml_unknown_device_family_type_highlighting_highlighting=warning +resharper_xaml_xaml_xamarin_forms_data_type_and_binding_context_type_mismatched_highlighting_highlighting=warning +resharper_xaml_x_key_attribute_disallowed_highlighting=error +resharper_xml_doc_comment_syntax_problem_highlighting=warning +resharper_xunit_xunit_test_with_console_output_highlighting=warning + +[*.{cshtml,htm,html,proto,razor}] +indent_style=tab +indent_size=tab +tab_width=4 + +[*.{asax,ascx,aspx,axaml,c,c++,cc,cginc,compute,cp,cpp,cs,css,cu,cuh,cxx,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,js,jsx,master,mpp,mq4,mq5,mqh,paml,skin,tpp,ts,tsx,usf,ush,vb,xaml,xamlx,xoml}] +indent_style=space +indent_size=4 +tab_width=4 + +[ "*.proto" ] +indent_style=tab +indent_size=tab +tab_width=4 + +[*.{asax,ascx,aspx,axaml,cs,cshtml,css,htm,html,js,jsx,master,paml,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}] +indent_style=space +indent_size=4 +tab_width=4 + +[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] +indent_style=space +indent_size=2 +tab_width=2 + +[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}] +indent_style=space +indent_size= 4 +tab_width= 4 +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +insert_final_newline = true diff --git a/Glamourer.Api/.gitignore b/Glamourer.Api/.gitignore new file mode 100644 index 0000000..3e16852 --- /dev/null +++ b/Glamourer.Api/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +.vs/ \ No newline at end of file diff --git a/Glamourer.Api/Api/IGlamourerApi.cs b/Glamourer.Api/Api/IGlamourerApi.cs new file mode 100644 index 0000000..cf4138f --- /dev/null +++ b/Glamourer.Api/Api/IGlamourerApi.cs @@ -0,0 +1,14 @@ +namespace Glamourer.Api.Api; + +/// The full API available. +public interface IGlamourerApi : IGlamourerApiBase +{ + /// + public IGlamourerApiDesigns Designs { get; } + + /// + public IGlamourerApiItems Items { get; } + + /// + public IGlamourerApiState State { get; } +} diff --git a/Glamourer.Api/Api/IGlamourerApiBase.cs b/Glamourer.Api/Api/IGlamourerApiBase.cs new file mode 100644 index 0000000..96b45d6 --- /dev/null +++ b/Glamourer.Api/Api/IGlamourerApiBase.cs @@ -0,0 +1,11 @@ +namespace Glamourer.Api.Api; + +/// Basic API functions. +public interface IGlamourerApiBase +{ + /// + /// Get the current API version of the Glamourer available in this installation. + /// Major version changes indicate incompatibilities, minor version changes are backward-compatible additions. + /// + public (int Major, int Minor) ApiVersion { get; } +} diff --git a/Glamourer.Api/Api/IGlamourerApiDesigns.cs b/Glamourer.Api/Api/IGlamourerApiDesigns.cs new file mode 100644 index 0000000..f12caeb --- /dev/null +++ b/Glamourer.Api/Api/IGlamourerApiDesigns.cs @@ -0,0 +1,33 @@ +using Glamourer.Api.Enums; + +namespace Glamourer.Api.Api; + +/// All functions related to Glamourer designs. +public interface IGlamourerApiDesigns +{ + /// Obtain a list of all available designs. + /// A dictionary of all designs from their GUID to their current display name. + public Dictionary GetDesignList(); + + /// Apply an existing design to an actor. + /// The GUID of the design to apply. + /// The game object index of the actor to be manipulated. + /// A key to unlock or lock the state if necessary. + /// The flags used for the reversion. Respects Once, Equipment, Customization, Lock (see .) + /// DesignNotFound, ActorNotFound, InvalidKey, Success. + public GlamourerApiEc ApplyDesign(Guid designId, int objectIndex, uint key, ApplyFlag flags); + + /// Apply an existing design to an actor. + /// The GUID of the design to apply. + /// The name of the players to be manipulated. + /// A key to unlock or lock the state if necessary. + /// The flags used for the reversion. Respects Once, Equipment, Customization, Lock (see .) + /// DesignNotFound, ActorNotFound, InvalidKey, Success. + /// /// + /// The player does not have to be currently available as long as he has a persisted Glamourer state.
+ /// Only players are checked for name equality, no NPCs.
+ /// If multiple players of the same name are found, all of them are reverted.
+ /// Prefer to use the index-based function unless you need to get the state of someone currently unavailable. + ///
+ public GlamourerApiEc ApplyDesignName(Guid designId, string playerName, uint key, ApplyFlag flags); +} diff --git a/Glamourer.Api/Api/IGlamourerApiItems.cs b/Glamourer.Api/Api/IGlamourerApiItems.cs new file mode 100644 index 0000000..1a880a8 --- /dev/null +++ b/Glamourer.Api/Api/IGlamourerApiItems.cs @@ -0,0 +1,80 @@ +using Glamourer.Api.Enums; + +namespace Glamourer.Api.Api; + +/// All functions related to items. +public interface IGlamourerApiItems +{ + /// Set a single item on an actor. + /// The game object index of the actor to be manipulated. + /// The slot to apply the item to. + /// The (Custom) ID of the item to apply. + /// The IDs of the stains to apply to the item. + /// A key to unlock or lock the state if necessary. + /// The flags used for the reversion. Respects Once (see .) + /// ItemInvalid, ActorNotFound, ActorNotHuman, InvalidKey, Success. + /// The item ID can be a custom item ID in Glamourer's format for models without an associated item, or a normal game item ID. + public GlamourerApiEc SetItem(int objectIndex, ApiEquipSlot slot, ulong itemId, IReadOnlyList stains, uint key, ApplyFlag flags); + + /// Set a single item on players. + /// The name of the players to be manipulated. + /// The slot to apply the item to. + /// The (Custom) ID of the item to apply. + /// The IDs of the stains to apply to the item. + /// A key to unlock or lock the state if necessary. + /// The flags used for the reversion. Respects Once (see .) + /// ItemInvalid, ActorNotFound, ActorNotHuman, InvalidKey, Success. + /// + /// The item ID can be a custom item ID in Glamourer's format for models without an associated item, or a normal game item ID.
+ /// The player does not have to be currently available as long as he has a persisted Glamourer state.
+ /// Only players are checked for name equality, no NPCs.
+ /// If multiple players of the same name are found, all of them are modified.
+ /// Prefer to use the index-based function unless you need to get the state of someone currently unavailable. + ///
+ public GlamourerApiEc SetItemName(string playerName, ApiEquipSlot slot, ulong itemId, IReadOnlyList stains, uint key, + ApplyFlag flags); + + /// Set a single bonus item on an actor. + /// The game object index of the actor to be manipulated. + /// The bonus slot to apply the item to. + /// The bonus item sheet ID of the item to apply (including stain). + /// A key to unlock or lock the state if necessary. + /// The flags used for the reversion. Respects Once (see .) + /// ItemInvalid, ActorNotFound, ActorNotHuman, InvalidKey, Success. + /// The bonus item ID can currently not be a custom item ID in Glamourer's format for models without an associated item. Use 0 to remove the bonus item. + public GlamourerApiEc SetBonusItem(int objectIndex, ApiBonusSlot slot, ulong bonusItemId, uint key, ApplyFlag flags); + + /// Set a single bonus item on an actor. + /// The game object index of the actor to be manipulated. + /// The bonus slot to apply the item to. + /// The bonus item sheet ID of the item to apply (including stain). + /// A key to unlock or lock the state if necessary. + /// The flags used for the reversion. Respects Once (see .) + /// ItemInvalid, ActorNotFound, ActorNotHuman, InvalidKey, Success. + /// + /// The bonus item ID can currently not be a custom item ID in Glamourer's format for models without an associated item. Use 0 to remove the bonus item.
+ /// The player does not have to be currently available as long as he has a persisted Glamourer state.
+ /// Only players are checked for name equality, no NPCs.
+ /// If multiple players of the same name are found, all of them are modified.
+ /// Prefer to use the index-based function unless you need to get the state of someone currently unavailable. + ///
+ public GlamourerApiEc SetBonusItemName(string playerName, ApiBonusSlot slot, ulong bonusItemId, uint key, ApplyFlag flags); + + /// Set the defined Meta State flags to the active or inactive state on actor. + /// The game object index of the actor to be manipulated. + /// The flags defining which meta states to update to the new value. This can be multiple at once. + /// The new value to update to. + /// A key to unlock or lock the state if necessary. + /// The flags used for the reversion. Respects Once (see .) + /// ItemInvalid, ActorNotFound, ActorNotHuman, InvalidKey, Success. + public GlamourerApiEc SetMetaState(int objectIndex, MetaFlag types, bool newValue, uint key, ApplyFlag flags); + + /// Set the defined Meta State flags to the active or inactive state on actor (by name) + /// The name of the players to be manipulated. + /// The flags defining which meta states to update to the new value. This can be multiple at once. + /// The new value to update to. + /// A key to unlock or lock the state if necessary. + /// The flags used for the reversion. Respects Once (see .) + /// ItemInvalid, ActorNotFound, ActorNotHuman, InvalidKey, Success. + public GlamourerApiEc SetMetaStateName(string playerName, MetaFlag types, bool newValue, uint key, ApplyFlag flags); +} diff --git a/Glamourer.Api/Api/IGlamourerApiState.cs b/Glamourer.Api/Api/IGlamourerApiState.cs new file mode 100644 index 0000000..a2fc059 --- /dev/null +++ b/Glamourer.Api/Api/IGlamourerApiState.cs @@ -0,0 +1,124 @@ +using Glamourer.Api.Enums; +using Newtonsoft.Json.Linq; + +namespace Glamourer.Api.Api; + +/// Any functions related to Glamourer's state tracking. +public interface IGlamourerApiState +{ + /// Get the current Glamourer state of an actor. + /// The game object index of the desired actor. + /// A key to unlock the state if necessary. + /// ActorNotFound, InvalidKey or Success, and the state on success. + /// The actor does not need to have a prior Glamourer state as long as it can be found. + public (GlamourerApiEc, JObject?) GetState(int objectIndex, uint key); + + /// Get the current Glamourer state of a player character. + /// The name of the desired player. + /// A key to unlock the state if necessary. + /// ActorNotFound, InvalidKey or Success, and the state on success. + /// + /// The player does not have to be currently available as long as he has a persisted Glamourer state. + /// Only players are checked for name equality, no NPCs. + /// If multiple players of the same name are found, the first is returned. + /// Prefer to use the index-based function unless you need to get the state of someone currently unavailable. + /// + public (GlamourerApiEc, JObject?) GetStateName(string playerName, uint key); + + /// + public (GlamourerApiEc, string?) GetStateBase64(int objectIndex, uint key); + + /// + public (GlamourerApiEc, string?) GetStateBase64Name(string objectName, uint key); + + /// Apply a supplied state to an actor. + /// The state, which can be either a Glamourer-supplied JObject or a Base64 string. + /// The game object index of the actor to be manipulated. + /// A key to unlock or lock the state if necessary. + /// The flags used for the application. Respects Once, Equipment, Customization and Lock (see .) + /// ActorNotFound, InvalidKey, ActorNotHuman, Success. + public GlamourerApiEc ApplyState(object applyState, int objectIndex, uint key, ApplyFlag flags); + + /// Apply a supplied state to players. + /// The state, which can be either a Glamourer-supplied JObject or a Base64 string. + /// The name of the player to be manipulated. + /// A key to unlock or lock the state if necessary. + /// The flags used for the application. Respects Once, Equipment, Customization and Lock (see .) + /// ActorNotFound, InvalidKey, ActorNotHuman, Success. + /// + /// The player does not have to be currently available as long as he has a persisted Glamourer state.
+ /// Only players are checked for name equality, no NPCs.
+ /// If multiple players of the same name are found, all of them are manipulated.
+ /// Prefer to use the index-based function unless you need to get the state of someone currently unavailable. + ///
+ public GlamourerApiEc ApplyStateName(object applyState, string playerName, uint key, ApplyFlag flags); + + /// Revert the Glamourer state of an actor to Game state. + /// The game object index of the actor to be manipulated. + /// A key to unlock the state if necessary. + /// The flags used for the reversion. Respects Equipment and Customization (see .) + /// ActorNotFound, InvalidKey, Success, NothingDone. + public GlamourerApiEc RevertState(int objectIndex, uint key, ApplyFlag flags); + + /// Revert the Glamourer state of players to game state. + /// The name of the players to be reverted. + /// A key to unlock the state if necessary. + /// The flags used for the reversion. Respects Equipment and Customization (see .) + /// ActorNotFound, InvalidKey, Success, NothingDone. + /// /// + /// The player does not have to be currently available as long as he has a persisted Glamourer state.
+ /// Only players are checked for name equality, no NPCs.
+ /// If multiple players of the same name are found, all of them are reverted.
+ /// Prefer to use the index-based function unless you need to get the state of someone currently unavailable. + ///
+ public GlamourerApiEc RevertStateName(string playerName, uint key, ApplyFlag flags); + + /// Unlock the Glamourer state of an actor with a key. + /// The game object index of the actor to be manipulated. + /// A key to unlock the state. + /// ActorNotFound, InvalidKey, Success, NothingDone. + public GlamourerApiEc UnlockState(int objectIndex, uint key); + + /// Unlock the Glamourer state of players with a key. + /// The name of the players to be unlocked. + /// A key to unlock the state. + /// InvalidKey, Success, NothingDone. + public GlamourerApiEc UnlockStateName(string playerName, uint key); + + /// Unlock all active glamourer states with a key. + /// The key to unlock states with. + /// The number of unlocked states. + public int UnlockAll(uint key); + + /// Revert the Glamourer state of an actor to automation state. + /// The game object index of the actor to be manipulated. + /// A key to unlock the state if necessary. + /// The flags used for the reversion. Respects Once and Lock (see .) + /// ActorNotFound, InvalidKey, Success, NothingDone. + public GlamourerApiEc RevertToAutomation(int objectIndex, uint key, ApplyFlag flags); + + /// Revert the Glamourer state of players to automation state. + /// The name of the players to be reverted. + /// A key to unlock the state if necessary. + /// The flags used for the reversion. Respects Once and Lock (see .) + /// ActorNotFound, InvalidKey, Success, NothingDone. + /// /// + /// The player does not have to be currently available as long as he has a persisted Glamourer state.
+ /// Only players are checked for name equality, no NPCs.
+ /// If multiple players of the same name are found, all of them are reverted.
+ /// Prefer to use the index-based function unless you need to get the state of someone currently unavailable. + ///
+ public GlamourerApiEc RevertToAutomationName(string playerName, uint key, ApplyFlag flags); + + /// Invoked with the game object pointer (if available) whenever an actors tracked state changes. + public event Action StateChanged; + + /// Invoked with the game object pointer (if available) whenever an actors tracked state changes, with the type of change. + public event Action StateChangedWithType; + + /// Invoked with the game object pointer (if available) whenever an actors tracked state finalizes a grouped change consisting of multiple smaller changes. + public event Action StateFinalized; + + /// Invoked when the player enters or leaves GPose (true => entered GPose, false => left GPose). + public event Action? GPoseChanged; +} diff --git a/Glamourer.Api/Enums/ApiBonusSlot.cs b/Glamourer.Api/Enums/ApiBonusSlot.cs new file mode 100644 index 0000000..84766cb --- /dev/null +++ b/Glamourer.Api/Enums/ApiBonusSlot.cs @@ -0,0 +1,11 @@ +namespace Glamourer.Api.Enums; + +/// Bonus item slots restricted to API-relevant slots. +public enum ApiBonusSlot : byte +{ + /// No slot. + Unknown = 0, + + /// The Glasses slot. + Glasses = 1, +} diff --git a/Glamourer.Api/Enums/ApiEquipSlot.cs b/Glamourer.Api/Enums/ApiEquipSlot.cs new file mode 100644 index 0000000..0b528af --- /dev/null +++ b/Glamourer.Api/Enums/ApiEquipSlot.cs @@ -0,0 +1,45 @@ +namespace Glamourer.Api.Enums; + +/// Equip slots restricted to API-relevant slots, but compatible with GameData.EquipSlots. +public enum ApiEquipSlot : byte +{ + /// No slot. + Unknown = 0, + + /// Mainhand, also used for both-handed weapons. + MainHand = 1, + + /// Offhand, used for shields or if you want to apply the offhand component of certain weapons. + OffHand = 2, + + /// Head. + Head = 3, + + /// Body. + Body = 4, + + /// Hands. + Hands = 5, + + /// Legs. + Legs = 7, + + /// Feet. + Feet = 8, + + /// Ears. + Ears = 9, + + /// Neck. + Neck = 10, + + /// Wrists. + Wrists = 11, + + /// Right Finger. + RFinger = 12, + + /// Left Finger. + /// Not officially existing, means "weapon could be equipped in either hand" for the game. + LFinger = 14, +} diff --git a/Glamourer.Api/Enums/ApplyFlag.cs b/Glamourer.Api/Enums/ApplyFlag.cs new file mode 100644 index 0000000..eb68b2a --- /dev/null +++ b/Glamourer.Api/Enums/ApplyFlag.cs @@ -0,0 +1,31 @@ +namespace Glamourer.Api.Enums; + +/// Application flags that can be used in different situations. +[Flags] +public enum ApplyFlag : ulong +{ + /// Apply the selected manipulation only once, without forcing the state into automation. + Once = 0x01, + + /// Apply the selected manipulation on the equipment (might be more or less supported). + Equipment = 0x02, + + /// Apply the selected manipulation on the customizations (might be more or less supported). + Customization = 0x04, + + /// Lock the state with the given key after applying the selected manipulation + Lock = 0x08, +} + +/// Extensions for apply flags. +public static class ApplyFlagEx +{ + /// The default application flags for design-based manipulations. + public const ApplyFlag DesignDefault = ApplyFlag.Once | ApplyFlag.Equipment | ApplyFlag.Customization; + + /// The default application flags for state-based manipulations. + public const ApplyFlag StateDefault = ApplyFlag.Equipment | ApplyFlag.Customization | ApplyFlag.Lock; + + /// The default application flags for reverse manipulations. + public const ApplyFlag RevertDefault = ApplyFlag.Equipment | ApplyFlag.Customization; +} diff --git a/Glamourer.Api/Enums/GlamourerApiEc.cs b/Glamourer.Api/Enums/GlamourerApiEc.cs new file mode 100644 index 0000000..76c52ac --- /dev/null +++ b/Glamourer.Api/Enums/GlamourerApiEc.cs @@ -0,0 +1,29 @@ +namespace Glamourer.Api.Enums; + +/// Return codes for API functions. +public enum GlamourerApiEc +{ + /// The function succeeded. + Success = 0, + + /// The function did not encounter a problem, but also did not do anything. + NothingDone = 1, + + /// The requested actor was not found. + ActorNotFound = 2, + + /// The requested actor was not human, but should have been. + ActorNotHuman, + + /// The requested design was not found. + DesignNotFound, + + /// The requested item was not found or could not be applied to the requested slot. + ItemInvalid, + + /// The state of an actor could not be manipulated because it was locked and the provided key could not unlock it. + InvalidKey, + + /// The provided object could not be converted into a valid Glamourer state to apply. + InvalidState, +} diff --git a/Glamourer.Api/Enums/SetMetaFlag.cs b/Glamourer.Api/Enums/SetMetaFlag.cs new file mode 100644 index 0000000..98158af --- /dev/null +++ b/Glamourer.Api/Enums/SetMetaFlag.cs @@ -0,0 +1,11 @@ +namespace Glamourer.Api.Enums; + +/// Application flags for setting the meta state of an actor. +[Flags] +public enum MetaFlag : ulong +{ + Wetness = 0x01, + HatState = 0x02, + VisorState = 0x04, + WeaponState = 0x08, +} diff --git a/Glamourer.Api/Enums/StateChangeType.cs b/Glamourer.Api/Enums/StateChangeType.cs new file mode 100644 index 0000000..55f5f6a --- /dev/null +++ b/Glamourer.Api/Enums/StateChangeType.cs @@ -0,0 +1,47 @@ +namespace Glamourer.Api.Enums; + +/// What type of information changed in a state. +public enum StateChangeType +{ + /// A characters saved state had the model id changed. This means everything may have changed. + Model = 0, + + /// A characters saved state had multiple customization values changed. + EntireCustomize = 1, + + /// A characters saved state had a customization value changed. + Customize = 2, + + /// A characters saved state had an equipment piece changed. + Equip = 3, + + /// A characters saved state had its weapons changed. + Weapon = 4, + + /// A characters saved state had a stain changed. + Stains = 5, + + /// A characters saved state had a crest visibility changed. + Crest = 6, + + /// A characters saved state had its customize parameter changed. + Parameter = 7, + + /// A characters saved state had a material color table value changed. + MaterialValue = 8, + + /// A characters saved state had a design applied. This means everything may have changed. + Design = 9, + + /// A characters saved state had its state reset to its game values. + Reset = 10, + + /// A characters saved state had a meta toggle changed. + Other = 11, + + /// A characters state was reapplied. Data is null. + Reapply = 12, + + /// A characters saved state had a bonus item changed. + BonusItem = 13, +} diff --git a/Glamourer.Api/Enums/StateFinalizationType.cs b/Glamourer.Api/Enums/StateFinalizationType.cs new file mode 100644 index 0000000..70c4eb7 --- /dev/null +++ b/Glamourer.Api/Enums/StateFinalizationType.cs @@ -0,0 +1,36 @@ +namespace Glamourer.Api.Enums; + +/// What type of Glamourer process was performed on the actors state to update it. +public enum StateFinalizationType +{ + /// A characters saved state had the model id altered. + ModelChange = 0, + + /// A singular Design was applied to an actors state. + DesignApplied = 1, + + /// A characters saved state had been reset to game values. + Revert = 2, + + /// A characters saved state had only its customization data reset to game state. + RevertCustomize = 3, + + /// A characters saved state had only its equipment data reset to game state. + RevertEquipment = 4, + + /// A characters saved state had its advanced values reverted to game state. + RevertAdvanced = 5, + + /// A characters saved state was reverted to automation state on top of their game state + RevertAutomation = 6, + + /// A characters saved state had a generic reapply as a single operation. + Reapply = 7, + + /// A characters saved state had their automation state reapplied over their existing state. + ReapplyAutomation = 8, + + /// A characters save state finished applying all updated slots for game state on gearset change or initial load. + Gearset = 9, +} + diff --git a/Glamourer.Api/Glamourer.Api.csproj b/Glamourer.Api/Glamourer.Api.csproj new file mode 100644 index 0000000..1d71ac6 --- /dev/null +++ b/Glamourer.Api/Glamourer.Api.csproj @@ -0,0 +1,34 @@ + + + Glamourer.Api + Glamourer + Copyright © 2025 + 2.4.1.0 + 2.4.1.0 + 2.4.1 + README.md + bin\$(Configuration)\ + + + + true + true + Glamourer.Api + Ottermandias + https://github.com/Ottermandias/Glamourer + Auxiliary functions for Glamourers external API. + MIT + + + + false + + + + 1591 + + + + + + diff --git a/Glamourer.Api/Glamourer.Api.csproj.DotSettings b/Glamourer.Api/Glamourer.Api.csproj.DotSettings new file mode 100644 index 0000000..7d7508c --- /dev/null +++ b/Glamourer.Api/Glamourer.Api.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/Glamourer.Api/GlobalUsings.cs b/Glamourer.Api/GlobalUsings.cs new file mode 100644 index 0000000..850275e --- /dev/null +++ b/Glamourer.Api/GlobalUsings.cs @@ -0,0 +1,4 @@ +// Global using directives + +global using System; +global using System.Collections.Generic; diff --git a/Glamourer.Api/Helpers/ActionSubscriber.cs b/Glamourer.Api/Helpers/ActionSubscriber.cs new file mode 100644 index 0000000..d6edcb2 --- /dev/null +++ b/Glamourer.Api/Helpers/ActionSubscriber.cs @@ -0,0 +1,114 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; + +namespace Glamourer.Api.Helpers; + +/// +/// Specialized subscriber only allowing to invoke actions. +/// +public class ActionSubscriber +{ + private readonly ICallGateSubscriber? _subscriber; + + /// Whether the subscriber could successfully be created. + public bool Valid + => _subscriber != null; + + protected ActionSubscriber(IDalamudPluginInterface pi, string label) + { + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// Invoke the action. See the source of the subscriber for details. + protected void Invoke() + => _subscriber?.InvokeAction(); +} + +/// +public class ActionSubscriber +{ + private readonly ICallGateSubscriber? _subscriber; + + /// Whether the subscriber could successfully be created. + public bool Valid + => _subscriber != null; + + protected ActionSubscriber(IDalamudPluginInterface pi, string label) + { + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// Invoke the action. See the source of the subscriber for details. + protected void Invoke(T1 a) + => _subscriber?.InvokeAction(a); +} + +/// +public class ActionSubscriber +{ + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + protected ActionSubscriber(IDalamudPluginInterface pi, string label) + { + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected void Invoke(T1 a, T2 b) + => _subscriber?.InvokeAction(a, b); +} + +/// +public class ActionSubscriber +{ + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + protected ActionSubscriber(IDalamudPluginInterface pi, string label) + { + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected void Invoke(T1 a, T2 b, T3 c) + => _subscriber?.InvokeAction(a, b, c); +} diff --git a/Glamourer.Api/Helpers/EventProvider.cs b/Glamourer.Api/Helpers/EventProvider.cs new file mode 100644 index 0000000..1ce3f81 --- /dev/null +++ b/Glamourer.Api/Helpers/EventProvider.cs @@ -0,0 +1,234 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Services; + +namespace Glamourer.Api.Helpers; + +/// +/// Specialized disposable Provider for Events. +/// Will execute the unsubscriber action on dispose if any is provided. +/// Can only be invoked and disposed. +/// +public sealed class EventProvider : IDisposable +{ + private readonly IPluginLog _log; + private ICallGateProvider? _provider; + private Delegate? _unsubscriber; + + public EventProvider(IDalamudPluginInterface pi, string label, (Action Add, Action Del)? subscribe = null) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + subscribe?.Add(Invoke); + _unsubscriber = subscribe?.Del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + public EventProvider(IDalamudPluginInterface pi, string label, Action add, Action del) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + add(this); + _unsubscriber = del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + /// Invoke the event. + public void Invoke() + { + try + { + _provider?.SendMessage(); + } + catch (Exception e) + { + _log.Error($"Exception thrown on IPC event:\n{e}"); + } + } + + public void Dispose() + { + switch (_unsubscriber) + { + case Action a: + a(Invoke); + break; + case Action b: + b(this); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize(this); + } + + ~EventProvider() + => Dispose(); +} + +/// +public sealed class EventProvider : IDisposable +{ + private readonly IPluginLog _log; + private ICallGateProvider? _provider; + private Delegate? _unsubscriber; + + public EventProvider(IDalamudPluginInterface pi, string label, (Action> Add, Action> Del)? subscribe = null) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + subscribe?.Add(Invoke); + _unsubscriber = subscribe?.Del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + public EventProvider(IDalamudPluginInterface pi, string label, Action> add, Action> del) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + add(this); + _unsubscriber = del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + /// + public void Invoke(T1 a) + { + try + { + _provider?.SendMessage(a); + } + catch (Exception e) + { + _log.Error($"Exception thrown on IPC event:\n{e}"); + } + } + + public void Dispose() + { + switch (_unsubscriber) + { + case Action> a: + a(Invoke); + break; + case Action> b: + b(this); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize(this); + } + + ~EventProvider() + => Dispose(); +} + +/// +public sealed class EventProvider : IDisposable +{ + private readonly IPluginLog _log; + private ICallGateProvider? _provider; + private Delegate? _unsubscriber; + + public EventProvider(IDalamudPluginInterface pi, string label, (Action> Add, Action> Del)? subscribe = null) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + subscribe?.Add(Invoke); + _unsubscriber = subscribe?.Del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + public EventProvider(IDalamudPluginInterface pi, string label, Action> add, Action> del) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + add(this); + _unsubscriber = del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + /// + public void Invoke(T1 a, T2 b) + { + try + { + _provider?.SendMessage(a, b); + } + catch (Exception e) + { + _log.Error($"Exception thrown on IPC event:\n{e}"); + } + } + + public void Dispose() + { + switch (_unsubscriber) + { + case Action> a: + a(Invoke); + break; + case Action> b: + b(this); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize(this); + } + + ~EventProvider() + => Dispose(); +} diff --git a/Glamourer.Api/Helpers/EventSubscriber.cs b/Glamourer.Api/Helpers/EventSubscriber.cs new file mode 100644 index 0000000..f91cc91 --- /dev/null +++ b/Glamourer.Api/Helpers/EventSubscriber.cs @@ -0,0 +1,394 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Services; + +namespace Glamourer.Api.Helpers; + +/// +/// Specialized disposable Subscriber for Events. +/// Subscriptions are wrapped to be individually exception-safe. +/// Can be enabled and disabled. +/// +public sealed class EventSubscriber : IDisposable +{ + private readonly string _label; + private readonly IPluginLog _log; + private readonly Dictionary _delegates = new(); + private ICallGateSubscriber? _subscriber; + private bool _disabled; + + public EventSubscriber(IDalamudPluginInterface pi, string label, params Action[] actions) + { + _label = label; + _log = PluginLogHelper.GetLog(pi); + try + { + _subscriber = pi.GetIpcSubscriber(label); + foreach (var action in actions) + Event += action; + + _disabled = false; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + /// Enable all currently subscribed actions registered with this EventSubscriber. + /// Does nothing if it is already enabled. + /// + public void Enable() + { + if (_disabled && _subscriber != null) + { + foreach (var action in _delegates.Values) + _subscriber.Subscribe(action); + + _disabled = false; + } + } + + /// + /// Disable all subscribed actions registered with this EventSubscriber. + /// Does nothing if it is already disabled. + /// Does not forget the actions, only disables them. + /// + public void Disable() + { + if (!_disabled) + { + if (_subscriber != null) + foreach (var action in _delegates.Values) + _subscriber.Unsubscribe(action); + + _disabled = true; + } + } + + /// + /// Add or remove an action to the IPC event, if it is valid. + /// + public event Action Event + { + add + { + if (_subscriber != null && !_delegates.ContainsKey(value)) + { + void Action() + { + try + { + value(); + } + catch (Exception e) + { + _log.Error($"Exception invoking IPC event {_label}:\n{e}"); + } + } + + if (_delegates.TryAdd(value, Action) && !_disabled) + _subscriber.Subscribe(Action); + } + } + remove + { + if (_subscriber != null && _delegates.Remove(value, out var action)) + _subscriber.Unsubscribe(action); + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +/// +public sealed class EventSubscriber : IDisposable +{ + private readonly string _label; + private readonly IPluginLog _log; + private readonly Dictionary, Action> _delegates = new(); + private ICallGateSubscriber? _subscriber; + private bool _disabled; + + public EventSubscriber(IDalamudPluginInterface pi, string label, params Action[] actions) + { + _label = label; + _log = PluginLogHelper.GetLog(pi); + try + { + _subscriber = pi.GetIpcSubscriber(label); + foreach (var action in actions) + Event += action; + + _disabled = false; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + public void Enable() + { + if (_disabled && _subscriber != null) + { + foreach (var action in _delegates.Values) + _subscriber.Subscribe(action); + + _disabled = false; + } + } + + /// + public void Disable() + { + if (!_disabled) + { + if (_subscriber != null) + foreach (var action in _delegates.Values) + _subscriber.Unsubscribe(action); + + _disabled = true; + } + } + + /// + public event Action Event + { + add + { + if (_subscriber != null && !_delegates.ContainsKey(value)) + { + void Action(T1 a) + { + try + { + value(a); + } + catch (Exception e) + { + _log.Error($"Exception invoking IPC event {_label}:\n{e}"); + } + } + + if (_delegates.TryAdd(value, Action) && !_disabled) + _subscriber.Subscribe(Action); + } + } + remove + { + if (_subscriber != null && _delegates.Remove(value, out var action)) + _subscriber.Unsubscribe(action); + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +/// +public sealed class EventSubscriber : IDisposable +{ + private readonly string _label; + private readonly IPluginLog _log; + private readonly Dictionary, Action> _delegates = new(); + private ICallGateSubscriber? _subscriber; + private bool _disabled; + + public EventSubscriber(IDalamudPluginInterface pi, string label, params Action[] actions) + { + _label = label; + _log = PluginLogHelper.GetLog(pi); + try + { + _subscriber = pi.GetIpcSubscriber(label); + foreach (var action in actions) + Event += action; + + _disabled = false; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + public void Enable() + { + if (_disabled && _subscriber != null) + { + foreach (var action in _delegates.Values) + _subscriber.Subscribe(action); + + _disabled = false; + } + } + + /// + public void Disable() + { + if (!_disabled) + { + if (_subscriber != null) + foreach (var action in _delegates.Values) + _subscriber.Unsubscribe(action); + + _disabled = true; + } + } + + /// + public event Action Event + { + add + { + if (_subscriber != null && !_delegates.ContainsKey(value)) + { + void Action(T1 a, T2 b) + { + try + { + value(a, b); + } + catch (Exception e) + { + _log.Error($"Exception invoking IPC event {_label}:\n{e}"); + } + } + + if (_delegates.TryAdd(value, Action) && !_disabled) + _subscriber.Subscribe(Action); + } + } + remove + { + if (_subscriber != null && _delegates.Remove(value, out var action)) + _subscriber.Unsubscribe(action); + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +/// +public sealed class EventSubscriber : IDisposable +{ + private readonly string _label; + private readonly IPluginLog _log; + private readonly Dictionary, Action> _delegates = new(); + private ICallGateSubscriber? _subscriber; + private bool _disabled; + + public EventSubscriber(IDalamudPluginInterface pi, string label, params Action[] actions) + { + _label = label; + _log = PluginLogHelper.GetLog(pi); + try + { + _subscriber = pi.GetIpcSubscriber(label); + foreach (var action in actions) + Event += action; + + _disabled = false; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + public void Enable() + { + if (_disabled && _subscriber != null) + { + foreach (var action in _delegates.Values) + _subscriber.Subscribe(action); + + _disabled = false; + } + } + + /// + public void Disable() + { + if (!_disabled) + { + if (_subscriber != null) + foreach (var action in _delegates.Values) + _subscriber.Unsubscribe(action); + + _disabled = true; + } + } + + /// + public event Action Event + { + add + { + if (_subscriber != null && !_delegates.ContainsKey(value)) + { + void Action(T1 a, T2 b, T3 c) + { + try + { + value(a, b, c); + } + catch (Exception e) + { + _log.Error($"Exception invoking IPC event {_label}:\n{e}"); + } + } + + if (_delegates.TryAdd(value, Action) && !_disabled) + _subscriber.Subscribe(Action); + } + } + remove + { + if (_subscriber != null && _delegates.Remove(value, out var action)) + _subscriber.Unsubscribe(action); + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} diff --git a/Glamourer.Api/Helpers/FuncProvider.cs b/Glamourer.Api/Helpers/FuncProvider.cs new file mode 100644 index 0000000..572f913 --- /dev/null +++ b/Glamourer.Api/Helpers/FuncProvider.cs @@ -0,0 +1,224 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; + +namespace Glamourer.Api.Helpers; + +/// +/// Specialized disposable Provider for Funcs. +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} + +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} + +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} + +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} + +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} + +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} + + +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} diff --git a/Glamourer.Api/Helpers/FuncSubscriber.cs b/Glamourer.Api/Helpers/FuncSubscriber.cs new file mode 100644 index 0000000..1d33601 --- /dev/null +++ b/Glamourer.Api/Helpers/FuncSubscriber.cs @@ -0,0 +1,217 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Ipc.Exceptions; + +namespace Glamourer.Api.Helpers; + +/// +/// Specialized subscriber only allowing to invoke functions with a return. +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// Whether the subscriber could successfully be created. + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// Invoke the function. See the source of the subscriber for details. + protected TRet Invoke() + => _subscriber != null ? _subscriber.InvokeFunc() : throw new IpcNotReadyError(_label); +} + +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected TRet Invoke(T1 a) + => _subscriber != null ? _subscriber.InvokeFunc(a) : throw new IpcNotReadyError(_label); +} + +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected TRet Invoke(T1 a, T2 b) + => _subscriber != null ? _subscriber.InvokeFunc(a, b) : throw new IpcNotReadyError(_label); +} + +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected TRet Invoke(T1 a, T2 b, T3 c) + => _subscriber != null ? _subscriber.InvokeFunc(a, b, c) : throw new IpcNotReadyError(_label); +} + +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected TRet Invoke(T1 a, T2 b, T3 c, T4 d) + => _subscriber != null ? _subscriber.InvokeFunc(a, b, c, d) : throw new IpcNotReadyError(_label); +} + +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected TRet Invoke(T1 a, T2 b, T3 c, T4 d, T5 e) + => _subscriber != null ? _subscriber.InvokeFunc(a, b, c, d, e) : throw new IpcNotReadyError(_label); +} + +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected TRet Invoke(T1 a, T2 b, T3 c, T4 d, T5 e, T6 f) + => _subscriber != null ? _subscriber.InvokeFunc(a, b, c, d, e, f) : throw new IpcNotReadyError(_label); +} diff --git a/Glamourer.Api/Helpers/PluginLogHelper.cs b/Glamourer.Api/Helpers/PluginLogHelper.cs new file mode 100644 index 0000000..98be78c --- /dev/null +++ b/Glamourer.Api/Helpers/PluginLogHelper.cs @@ -0,0 +1,26 @@ +using Dalamud.IoC; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; + +namespace Glamourer.Api.Helpers; + +internal class PluginLogHelper +{ + [PluginService] + private static IPluginLog? _log { get; set; } + + private PluginLogHelper(IDalamudPluginInterface pi) + => pi.Inject(this); + + public static void WriteError(IDalamudPluginInterface pi, string errorMessage) + => GetLog(pi).Error(errorMessage); + + public static IPluginLog GetLog(IDalamudPluginInterface pi) + { + if (_log != null) + return _log; + + _ = new PluginLogHelper(pi); + return _log!; + } +} diff --git a/Glamourer.Api/IpcSubscribers/Designs.cs b/Glamourer.Api/IpcSubscribers/Designs.cs new file mode 100644 index 0000000..44c3846 --- /dev/null +++ b/Glamourer.Api/IpcSubscribers/Designs.cs @@ -0,0 +1,52 @@ +using Dalamud.Plugin; +using Glamourer.Api.Api; +using Glamourer.Api.Enums; +using Glamourer.Api.Helpers; + +namespace Glamourer.Api.IpcSubscribers; + +/// +public sealed class GetDesignList(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(GetDesignList)}.V2"; + + /// + public new Dictionary Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider> Provider(IDalamudPluginInterface pi, IGlamourerApiDesigns api) + => new(pi, Label, api.GetDesignList); +} + +/// +public sealed class ApplyDesign(IDalamudPluginInterface pi) : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(ApplyDesign)}"; + + /// + public GlamourerApiEc Invoke(Guid designId, int objectIndex, uint key = 0, ApplyFlag flags = ApplyFlagEx.DesignDefault) + => (GlamourerApiEc)Invoke(designId, objectIndex, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiDesigns api) + => new(pi, Label, (a, b, c, d) => (int)api.ApplyDesign(a, b, c, (ApplyFlag)d)); +} + +/// +public sealed class ApplyDesignName(IDalamudPluginInterface pi) : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(ApplyDesignName)}"; + + /// + public GlamourerApiEc Invoke(Guid designId, string objectName, uint key = 0, ApplyFlag flags = ApplyFlagEx.DesignDefault) + => (GlamourerApiEc)Invoke(designId, objectName, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiDesigns api) + => new(pi, Label, (a, b, c, d) => (int)api.ApplyDesignName(a, b, c, (ApplyFlag)d)); +} diff --git a/Glamourer.Api/IpcSubscribers/Items.cs b/Glamourer.Api/IpcSubscribers/Items.cs new file mode 100644 index 0000000..c56475c --- /dev/null +++ b/Glamourer.Api/IpcSubscribers/Items.cs @@ -0,0 +1,110 @@ +using Dalamud.Plugin; +using Glamourer.Api.Api; +using Glamourer.Api.Enums; +using Glamourer.Api.Helpers; + +namespace Glamourer.Api.IpcSubscribers; + +/// +public sealed class SetItem(IDalamudPluginInterface pi) + : FuncSubscriber, uint, ulong, int>(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(SetItem)}.V3"; + + /// + public GlamourerApiEc Invoke(int objectIndex, ApiEquipSlot slot, ulong itemId, IReadOnlyList stain, uint key = 0, + ApplyFlag flags = ApplyFlag.Once) + => (GlamourerApiEc)Invoke(objectIndex, (byte)slot, itemId, stain, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider, uint, ulong, int> Provider(IDalamudPluginInterface pi, + IGlamourerApiItems api) + => new(pi, Label, (a, b, c, d, e, f) => (int)api.SetItem(a, (ApiEquipSlot)b, c, d, e, (ApplyFlag)f)); +} + +/// +public sealed class SetItemName(IDalamudPluginInterface pi) + : FuncSubscriber, uint, ulong, int>(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(SetItemName)}.V2"; + + /// + public GlamourerApiEc Invoke(string objectName, ApiEquipSlot slot, ulong itemId, IReadOnlyList stain, uint key = 0, + ApplyFlag flags = ApplyFlag.Once) + => (GlamourerApiEc)Invoke(objectName, (byte)slot, itemId, stain, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider, uint, ulong, int> Provider(IDalamudPluginInterface pi, + IGlamourerApiItems api) + => new(pi, Label, (a, b, c, d, e, f) => (int)api.SetItemName(a, (ApiEquipSlot)b, c, d, e, (ApplyFlag)f)); +} + +/// +public sealed class SetBonusItem(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(SetBonusItem)}"; + + /// + public GlamourerApiEc Invoke(int objectIndex, ApiBonusSlot slot, ulong itemId, uint key = 0, ApplyFlag flags = ApplyFlag.Once) + => (GlamourerApiEc)Invoke(objectIndex, (byte)slot, itemId, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiItems api) + => new(pi, Label, (a, b, c, d, e) => (int)api.SetBonusItem(a, (ApiBonusSlot)b, c, d, (ApplyFlag)e)); +} + +/// +public sealed class SetBonusItemName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(SetBonusItemName)}.V2"; + + /// + public GlamourerApiEc Invoke(string objectName, ApiBonusSlot slot, ulong itemId, uint key = 0, ApplyFlag flags = ApplyFlag.Once) + => (GlamourerApiEc)Invoke(objectName, (byte)slot, itemId, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiItems api) + => new(pi, Label, (a, b, c, d, e) => (int)api.SetBonusItemName(a, (ApiBonusSlot)b, c, d, (ApplyFlag)e)); +} + +/// +public sealed class SetMetaState(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(SetMetaState)}"; + + /// + public GlamourerApiEc Invoke(int objectIndex, MetaFlag types, bool newValue, uint key = 0, + ApplyFlag flags = ApplyFlag.Once) + => (GlamourerApiEc)Invoke(objectIndex, (ulong)types, newValue, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, + IGlamourerApiItems api) + => new(pi, Label, (a, b, c, d, e) => (int)api.SetMetaState(a, (MetaFlag)b, c, d, (ApplyFlag)e)); +} + +/// +public sealed class SetMetaStateName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(SetMetaStateName)}"; + + /// + public GlamourerApiEc Invoke(string objectName, MetaFlag types, bool newValue, uint key = 0, + ApplyFlag flags = ApplyFlag.Once) + => (GlamourerApiEc)Invoke(objectName, (ulong)types, newValue, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, + IGlamourerApiItems api) + => new(pi, Label, (a, b, c, d, e) => (int)api.SetMetaStateName(a, (MetaFlag)b, c, d, (ApplyFlag)e)); +} diff --git a/Glamourer.Api/IpcSubscribers/Legacy/Designs.cs b/Glamourer.Api/IpcSubscribers/Legacy/Designs.cs new file mode 100644 index 0000000..9fc479b --- /dev/null +++ b/Glamourer.Api/IpcSubscribers/Legacy/Designs.cs @@ -0,0 +1,52 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using Glamourer.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Glamourer.Api.IpcSubscribers.Legacy; + +public sealed class GetDesignList(IDalamudPluginInterface pi) + : FuncSubscriber<(string Name, Guid Identifier)[]>(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(GetDesignList)}"; + + public new (string Name, Guid Identifier)[] Invoke() + => base.Invoke(); +} + +public sealed class ApplyByGuid(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyByGuid)}"; + + public new void Invoke(Guid design, string name) + => base.Invoke(design, name); +} + +public sealed class ApplyByGuidOnce(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyByGuidOnce)}"; + + public new void Invoke(Guid design, string name) + => base.Invoke(design, name); +} + +public sealed class ApplyByGuidToCharacter(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyByGuidToCharacter)}"; + + public new void Invoke(Guid design, ICharacter? character) + => base.Invoke(design, character); +} + +public sealed class ApplyByGuidOnceToCharacter(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyByGuidOnceToCharacter)}"; + + public new void Invoke(Guid design, ICharacter? character) + => base.Invoke(design, character); +} diff --git a/Glamourer.Api/IpcSubscribers/Legacy/Items.cs b/Glamourer.Api/IpcSubscribers/Legacy/Items.cs new file mode 100644 index 0000000..6a9661a --- /dev/null +++ b/Glamourer.Api/IpcSubscribers/Legacy/Items.cs @@ -0,0 +1,66 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using Glamourer.Api.Api; +using Glamourer.Api.Enums; +using Glamourer.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Glamourer.Api.IpcSubscribers.Legacy; + +public sealed class SetItem(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(SetItem)}"; + + public new GlamourerApiEc Invoke(ICharacter? character, byte slot, ulong itemId, byte stainId, uint key) + => (GlamourerApiEc)base.Invoke(character, slot, itemId, stainId, key); +} + +public sealed class SetItemOnce(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(SetItemOnce)}"; + + public new GlamourerApiEc Invoke(ICharacter? character, byte slot, ulong itemId, byte stainId, uint key) + => (GlamourerApiEc)base.Invoke(character, slot, itemId, stainId, key); +} + +public sealed class SetItemByActorName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(SetItemByActorName)}"; + + public new GlamourerApiEc Invoke(string actorName, byte slot, ulong itemId, byte stainId, uint key) + => (GlamourerApiEc)base.Invoke(actorName, slot, itemId, stainId, key); +} + +public sealed class SetItemOnceByActorName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(SetItemOnceByActorName)}"; + + public new GlamourerApiEc Invoke(string actorName, byte slot, ulong itemId, byte stainId, uint key) + => (GlamourerApiEc)base.Invoke(actorName, slot, itemId, stainId, key); +} + +public sealed class SetItemV2(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(SetItem)}.V2"; + + public GlamourerApiEc Invoke(int objectIndex, ApiEquipSlot slot, ulong itemId, byte stain, uint key = 0, ApplyFlag flags = ApplyFlag.Once) + => (GlamourerApiEc)Invoke(objectIndex, (byte)slot, itemId, stain, key, (ulong)flags); +} + +public sealed class SetItemName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(SetItemName)}"; + + public GlamourerApiEc Invoke(string objectName, ApiEquipSlot slot, ulong itemId, byte stain, uint key = 0, ApplyFlag flags = ApplyFlag.Once) + => (GlamourerApiEc)Invoke(objectName, (byte)slot, itemId, stain, key, (ulong)flags); + + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiItems api) + => new(pi, Label, (a, b, c, d, e, f) => (int)api.SetItemName(a, (ApiEquipSlot)b, c, [d], e, (ApplyFlag)f)); +} diff --git a/Glamourer.Api/IpcSubscribers/Legacy/PluginState.cs b/Glamourer.Api/IpcSubscribers/Legacy/PluginState.cs new file mode 100644 index 0000000..4bf8fe8 --- /dev/null +++ b/Glamourer.Api/IpcSubscribers/Legacy/PluginState.cs @@ -0,0 +1,15 @@ +using Dalamud.Plugin; +using Glamourer.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Glamourer.Api.IpcSubscribers.Legacy; + +public sealed class ApiVersions(IDalamudPluginInterface pi) + : FuncSubscriber<(int, int)>(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApiVersions)}"; + + public new (int Major, int Minor) Invoke() + => base.Invoke(); +} diff --git a/Glamourer.Api/IpcSubscribers/Legacy/State.cs b/Glamourer.Api/IpcSubscribers/Legacy/State.cs new file mode 100644 index 0000000..2208e44 --- /dev/null +++ b/Glamourer.Api/IpcSubscribers/Legacy/State.cs @@ -0,0 +1,250 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using Glamourer.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Glamourer.Api.IpcSubscribers.Legacy; + +public sealed class Revert(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(Revert)}"; + + public new void Invoke(string characterName) + => base.Invoke(characterName); +} + +public sealed class RevertCharacter(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(RevertCharacter)}"; + + public new void Invoke(ICharacter? character) + => base.Invoke(character); +} + +public sealed class RevertLock(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(RevertLock)}"; + + public new void Invoke(string characterName, uint key) + => base.Invoke(characterName, key); +} + +public sealed class RevertCharacterLock(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(RevertCharacterLock)}"; + + public new void Invoke(ICharacter? character, uint key) + => base.Invoke(character, key); +} + +public sealed class RevertToAutomation(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(RevertToAutomation)}"; + + public new bool Invoke(string characterName, uint key) + => base.Invoke(characterName, key); +} + +public sealed class RevertToAutomationCharacter(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(RevertToAutomationCharacter)}"; + + public new bool Invoke(ICharacter? character, uint key) + => base.Invoke(character, key); +} + +public sealed class Unlock(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(Unlock)}"; + + public new bool Invoke(ICharacter? character, uint key) + => base.Invoke(character, key); +} + +public sealed class UnlockName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(UnlockName)}"; + + public new bool Invoke(string characterName, uint key) + => base.Invoke(characterName, key); +} + +public static class StateChanged +{ + public const string Label = $"Penumbra.{nameof(StateChanged)}"; + + public static EventSubscriber> Subscriber(IDalamudPluginInterface pi, + params Action>[] actions) + => new(pi, Label, actions); +} + +public sealed class GetAllCustomization(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(GetAllCustomization)}"; + + public new string? Invoke(string characterName) + => base.Invoke(characterName); +} + +public sealed class GetAllCustomizationFromCharacter(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(GetAllCustomizationFromCharacter)}"; + + public new string? Invoke(ICharacter? character) + => base.Invoke(character); +} + +public sealed class GetAllCustomizationLocked(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(GetAllCustomizationLocked)}"; + + public new string? Invoke(string characterName, uint key) + => base.Invoke(characterName, key); +} + +public sealed class GetAllCustomizationFromLockedCharacter(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(GetAllCustomizationFromLockedCharacter)}"; + + public new string? Invoke(ICharacter? character, uint key) + => base.Invoke(character, key); +} + +public sealed class ApplyAll(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyAll)}"; + + public new void Invoke(string characterName, string stateBase64) + => base.Invoke(characterName, stateBase64); +} + +public sealed class ApplyAllOnce(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyAllOnce)}"; + + public new void Invoke(string characterName, string stateBase64) + => base.Invoke(characterName, stateBase64); +} + +public sealed class ApplyAllToCharacter(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyAllToCharacter)}"; + + public new void Invoke(ICharacter? character, string stateBase64) + => base.Invoke(character, stateBase64); +} + +public sealed class ApplyAllOnceToCharacter(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyAllOnceToCharacter)}"; + + public new void Invoke(ICharacter? character, string stateBase64) + => base.Invoke(character, stateBase64); +} + +public sealed class ApplyOnlyEquipment(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyOnlyEquipment)}"; + + public new void Invoke(string characterName, string stateBase64) + => base.Invoke(characterName, stateBase64); +} + +public sealed class ApplyOnlyEquipmentToCharacter(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyOnlyEquipmentToCharacter)}"; + + public new void Invoke(ICharacter? character, string stateBase64) + => base.Invoke(character, stateBase64); +} + +public sealed class ApplyOnlyCustomization(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyOnlyCustomization)}"; + + public new void Invoke(string characterName, string stateBase64) + => base.Invoke(characterName, stateBase64); +} + +public sealed class ApplyOnlyCustomizationToCharacter(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyOnlyCustomizationToCharacter)}"; + + public new void Invoke(ICharacter? character, string stateBase64) + => base.Invoke(character, stateBase64); +} + +public sealed class ApplyAllLock(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyAllLock)}"; + + public new void Invoke(string characterName, string stateBase64, uint key) + => base.Invoke(characterName, stateBase64, key); +} + +public sealed class ApplyAllToCharacterLock(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyAllToCharacterLock)}"; + + public new void Invoke(ICharacter? character, string stateBase64, uint key) + => base.Invoke(character, stateBase64, key); +} + +public sealed class ApplyOnlyEquipmentLock(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyOnlyEquipmentLock)}"; + + public new void Invoke(string characterName, string stateBase64, uint key) + => base.Invoke(characterName, stateBase64, key); +} + +public sealed class ApplyOnlyEquipmentToCharacterLock(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyOnlyEquipmentToCharacterLock)}"; + + public new void Invoke(ICharacter? character, string stateBase64, uint key) + => base.Invoke(character, stateBase64, key); +} + +public sealed class ApplyOnlyCustomizationLock(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyOnlyCustomizationLock)}"; + + public new void Invoke(string characterName, string stateBase64, uint key) + => base.Invoke(characterName, stateBase64, key); +} + +public sealed class ApplyOnlyCustomizationToCharacterLock(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Glamourer.{nameof(ApplyOnlyCustomizationToCharacterLock)}"; + + public new void Invoke(ICharacter? character, string stateBase64, uint key) + => base.Invoke(character, stateBase64, key); +} diff --git a/Glamourer.Api/IpcSubscribers/PluginState.cs b/Glamourer.Api/IpcSubscribers/PluginState.cs new file mode 100644 index 0000000..3ec6890 --- /dev/null +++ b/Glamourer.Api/IpcSubscribers/PluginState.cs @@ -0,0 +1,51 @@ +using Dalamud.Plugin; +using Glamourer.Api.Api; +using Glamourer.Api.Helpers; + +namespace Glamourer.Api.IpcSubscribers; + +/// +public sealed class ApiVersion(IDalamudPluginInterface pi) + : FuncSubscriber<(int, int)>(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(ApiVersion)}.V2"; + + /// + public new (int Major, int Minor) Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider<(int, int)> Provider(IDalamudPluginInterface pi, IGlamourerApiBase api) + => new(pi, Label, () => api.ApiVersion); +} + +/// Triggered when the Glamourer API is initialized and ready. +public static class Initialized +{ + /// The label. + public const string Label = $"Glamourer.{nameof(Initialized)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi) + => new(pi, Label); +} + +/// Triggered when the Glamourer API is fully disposed and unavailable. +public static class Disposed +{ + /// The label. + public const string Label = $"Glamourer.{nameof(Disposed)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi) + => new(pi, Label); +} diff --git a/Glamourer.Api/IpcSubscribers/State.cs b/Glamourer.Api/IpcSubscribers/State.cs new file mode 100644 index 0000000..dd9a020 --- /dev/null +++ b/Glamourer.Api/IpcSubscribers/State.cs @@ -0,0 +1,311 @@ +using Dalamud.Plugin; +using Glamourer.Api.Api; +using Glamourer.Api.Enums; +using Glamourer.Api.Helpers; +using Newtonsoft.Json.Linq; + +namespace Glamourer.Api.IpcSubscribers; + +/// +public sealed class GetState(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(GetState)}"; + + /// + public new (GlamourerApiEc, JObject?) Invoke(int objectIndex, uint key = 0) + { + var (ec, data) = base.Invoke(objectIndex, key); + return ((GlamourerApiEc)ec, data); + } + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (a, b) => + { + var (ec, data) = api.GetState(a, b); + return ((int)ec, data); + }); +} + +/// +public sealed class GetStateName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(GetStateName)}"; + + /// + public new (GlamourerApiEc, JObject?) Invoke(string objectName, uint key = 0) + { + var (ec, data) = base.Invoke(objectName, key); + return ((GlamourerApiEc)ec, data); + } + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (i, k) => + { + var (ec, data) = api.GetStateName(i, k); + return ((int)ec, data); + }); +} + +/// +public sealed class GetStateBase64(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(GetStateBase64)}"; + + /// + public new (GlamourerApiEc, string?) Invoke(int objectIndex, uint key = 0) + { + var (ec, data) = base.Invoke(objectIndex, key); + return ((GlamourerApiEc)ec, data); + } + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (a, b) => + { + var (ec, data) = api.GetStateBase64(a, b); + return ((int)ec, data); + }); +} + +/// +public sealed class GetStateBase64Name(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(GetStateBase64Name)}"; + + /// + public new (GlamourerApiEc, string?) Invoke(string objectName, uint key = 0) + { + var (ec, data) = base.Invoke(objectName, key); + return ((GlamourerApiEc)ec, data); + } + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (i, k) => + { + var (ec, data) = api.GetStateBase64Name(i, k); + return ((int)ec, data); + }); +} + +/// +public sealed class ApplyState(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(ApplyState)}"; + + /// + public GlamourerApiEc Invoke(JObject state, int objectIndex, uint key = 0, ApplyFlag flags = ApplyFlagEx.StateDefault) + => (GlamourerApiEc)Invoke(state, objectIndex, key, (ulong)flags); + + /// + public GlamourerApiEc Invoke(string base64State, int objectIndex, uint key = 0, ApplyFlag flags = ApplyFlagEx.StateDefault) + => (GlamourerApiEc)Invoke(base64State, objectIndex, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (a, b, c, d) => (int)api.ApplyState(a, b, c, (ApplyFlag)d)); +} + +/// +public sealed class ApplyStateName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(ApplyStateName)}"; + + /// + public GlamourerApiEc Invoke(JObject state, string objectName, uint key = 0, ApplyFlag flags = ApplyFlagEx.StateDefault) + => (GlamourerApiEc)Invoke(state, objectName, key, (ulong)flags); + + /// + public GlamourerApiEc Invoke(string base64State, string objectName, uint key = 0, ApplyFlag flags = ApplyFlagEx.StateDefault) + => (GlamourerApiEc)Invoke(base64State, objectName, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (a, b, c, d) => (int)api.ApplyStateName(a, b, c, (ApplyFlag)d)); +} + +/// +public sealed class RevertState(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(RevertState)}"; + + /// + public GlamourerApiEc Invoke(int objectIndex, uint key = 0, ApplyFlag flags = ApplyFlagEx.RevertDefault) + => (GlamourerApiEc)Invoke(objectIndex, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (a, b, c) => (int)api.RevertState(a, b, (ApplyFlag)c)); +} + +/// +public sealed class RevertStateName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(RevertStateName)}"; + + /// + public GlamourerApiEc Invoke(string objectName, uint key = 0, ApplyFlag flags = ApplyFlagEx.RevertDefault) + => (GlamourerApiEc)Invoke(objectName, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (a, b, c) => (int)api.RevertStateName(a, b, (ApplyFlag)c)); +} + +/// +public sealed class UnlockState(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(UnlockState)}"; + + /// + public new GlamourerApiEc Invoke(int objectIndex, uint key = 0) + => (GlamourerApiEc)base.Invoke(objectIndex, key); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (a, b) => (int)api.UnlockState(a, b)); +} + +/// +public sealed class UnlockStateName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(UnlockStateName)}"; + + /// + public new GlamourerApiEc Invoke(string objectName, uint key = 0) + => (GlamourerApiEc)base.Invoke(objectName, key); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (a, b) => (int)api.UnlockStateName(a, b)); +} + +/// +public sealed class UnlockAll(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(UnlockAll)}"; + + /// + public new int Invoke(uint key) + => base.Invoke(key); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, api.UnlockAll); +} + +/// +public sealed class RevertToAutomation(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(RevertToAutomation)}.V2"; + + /// + public GlamourerApiEc Invoke(int objectIndex, uint key = 0, ApplyFlag flags = ApplyFlagEx.RevertDefault) + => (GlamourerApiEc)Invoke(objectIndex, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (a, b, c) => (int)api.RevertToAutomation(a, b, (ApplyFlag)c)); +} + +/// +public sealed class RevertToAutomationName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Glamourer.{nameof(RevertToAutomationName)}"; + + /// + public GlamourerApiEc Invoke(string objectName, uint key = 0, ApplyFlag flags = ApplyFlagEx.RevertDefault) + => (GlamourerApiEc)Invoke(objectName, key, (ulong)flags); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (a, b, c) => (int)api.RevertToAutomationName(a, b, (ApplyFlag)c)); +} + +/// +public static class StateChanged +{ + /// The label. + public const string Label = $"Penumbra.{nameof(StateChanged)}.V2"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (t => api.StateChanged += t, t => api.StateChanged -= t)); +} + +/// +public static class StateChangedWithType +{ + /// The label. + public const string Label = $"Penumbra.{nameof(StateChangedWithType)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (t => api.StateChangedWithType += t, t => api.StateChangedWithType -= t)); +} + +/// +public static class StateFinalized +{ + /// The label. + public const string Label = $"Penumbra.{nameof(StateFinalized)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (t => api.StateFinalized += t, t => api.StateFinalized -= t)); +} + +/// +public static class GPoseChanged +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GPoseChanged)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IGlamourerApiState api) + => new(pi, Label, (t => api.GPoseChanged += t, t => api.GPoseChanged -= t)); +} diff --git a/Glamourer.Api/README.md b/Glamourer.Api/README.md new file mode 100644 index 0000000..e49aaaf --- /dev/null +++ b/Glamourer.Api/README.md @@ -0,0 +1,4 @@ +# Glamourer + +This is an auxiliary repository for Glamourers external API. +For more information, see the [main repo](https://github.com/Ottermandias/Glamourer). \ No newline at end of file diff --git a/Glamourer.Api/packages.lock.json b/Glamourer.Api/packages.lock.json new file mode 100644 index 0000000..bd07e56 --- /dev/null +++ b/Glamourer.Api/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + "net9.0-windows7.0": { + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.25, )", + "resolved": "1.2.25", + "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + } + } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fe28402 --- /dev/null +++ b/LICENSE @@ -0,0 +1,617 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. \ No newline at end of file diff --git a/LICENSE_MIT b/LICENSE_MIT new file mode 100644 index 0000000..5e5732c --- /dev/null +++ b/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Penumbra-Sync + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MareAPI/.gitignore b/MareAPI/.gitignore new file mode 100644 index 0000000..dfcfd56 --- /dev/null +++ b/MareAPI/.gitignore @@ -0,0 +1,350 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ diff --git a/MareAPI/LICENSE b/MareAPI/LICENSE new file mode 100644 index 0000000..f0d7bf5 --- /dev/null +++ b/MareAPI/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Mare Synchronos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MareAPI/MareSynchronosAPI/Data/CharacterData.cs b/MareAPI/MareSynchronosAPI/Data/CharacterData.cs new file mode 100644 index 0000000..da155a3 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/CharacterData.cs @@ -0,0 +1,36 @@ +using MareSynchronos.API.Data.Enum; +using MessagePack; +using System.Text.Json.Serialization; +using System.Text.Json; +using System.Text; +using System.Security.Cryptography; + +namespace MareSynchronos.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public class CharacterData +{ + public CharacterData() + { + DataHash = new(() => + { + var json = JsonSerializer.Serialize(this); +#pragma warning disable SYSLIB0021 // Type or member is obsolete + using SHA256CryptoServiceProvider cryptoProvider = new(); +#pragma warning restore SYSLIB0021 // Type or member is obsolete + return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(json))).Replace("-", "", StringComparison.Ordinal); + }); + } + + public Dictionary CustomizePlusData { get; set; } = new(); + [JsonIgnore] + public Lazy DataHash { get; } + + public Dictionary> FileReplacements { get; set; } = new(); + public Dictionary GlamourerData { get; set; } = new(); + public string HeelsData { get; set; } = string.Empty; + public string HonorificData { get; set; } = string.Empty; + public string ManipulationData { get; set; } = string.Empty; + public string MoodlesData { get; set; } = string.Empty; + public string PetNamesData { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/ChatMessage.cs b/MareAPI/MareSynchronosAPI/Data/ChatMessage.cs new file mode 100644 index 0000000..55224a5 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/ChatMessage.cs @@ -0,0 +1,11 @@ +using MessagePack; + +namespace MareSynchronos.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public record ChatMessage +{ + public string SenderName { get; set; } = string.Empty; + public uint SenderHomeWorldId { get; set; } = 0; + public byte[] PayloadContent { get; set; } = []; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs b/MareAPI/MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs new file mode 100644 index 0000000..dfd0456 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs @@ -0,0 +1,19 @@ +namespace MareSynchronos.API.Data.Comparer; + +public class GroupDataComparer : IEqualityComparer +{ + public static GroupDataComparer Instance => _instance; + private static GroupDataComparer _instance = new GroupDataComparer(); + + private GroupDataComparer() { } + public bool Equals(GroupData? x, GroupData? y) + { + if (x == null || y == null) return false; + return x.GID.Equals(y.GID, StringComparison.Ordinal); + } + + public int GetHashCode(GroupData obj) + { + return obj.GID.GetHashCode(); + } +} diff --git a/MareAPI/MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs b/MareAPI/MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs new file mode 100644 index 0000000..3814c6f --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs @@ -0,0 +1,23 @@ +using MareSynchronos.API.Dto.Group; + +namespace MareSynchronos.API.Data.Comparer; + + +public class GroupDtoComparer : IEqualityComparer +{ + public static GroupDtoComparer Instance => _instance; + private static GroupDtoComparer _instance = new GroupDtoComparer(); + + private GroupDtoComparer() { } + + public bool Equals(GroupDto? x, GroupDto? y) + { + if (x == null || y == null) return false; + return x.GID.Equals(y.GID, StringComparison.Ordinal); + } + + public int GetHashCode(GroupDto obj) + { + return obj.Group.GID.GetHashCode(); + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs b/MareAPI/MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs new file mode 100644 index 0000000..c1dde50 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs @@ -0,0 +1,20 @@ +using MareSynchronos.API.Dto.Group; + +namespace MareSynchronos.API.Data.Comparer; + +public class GroupPairDtoComparer : IEqualityComparer +{ + public static GroupPairDtoComparer Instance => _instance; + private static GroupPairDtoComparer _instance = new(); + private GroupPairDtoComparer() { } + public bool Equals(GroupPairDto? x, GroupPairDto? y) + { + if (x == null || y == null) return false; + return x.GID.Equals(y.GID, StringComparison.Ordinal) && x.UID.Equals(y.UID, StringComparison.Ordinal); + } + + public int GetHashCode(GroupPairDto obj) + { + return HashCode.Combine(obj.Group.GID.GetHashCode(), obj.User.UID.GetHashCode()); + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Comparer/UserDataComparer.cs b/MareAPI/MareSynchronosAPI/Data/Comparer/UserDataComparer.cs new file mode 100644 index 0000000..68aa227 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Comparer/UserDataComparer.cs @@ -0,0 +1,20 @@ +namespace MareSynchronos.API.Data.Comparer; + +public class UserDataComparer : IEqualityComparer +{ + public static UserDataComparer Instance => _instance; + private static UserDataComparer _instance = new(); + + private UserDataComparer() { } + + public bool Equals(UserData? x, UserData? y) + { + if (x == null || y == null) return false; + return x.UID.Equals(y.UID, StringComparison.Ordinal); + } + + public int GetHashCode(UserData obj) + { + return obj.UID.GetHashCode(); + } +} diff --git a/MareAPI/MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs b/MareAPI/MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs new file mode 100644 index 0000000..9c8451c --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs @@ -0,0 +1,20 @@ +using MareSynchronos.API.Dto.User; + +namespace MareSynchronos.API.Data.Comparer; + +public class UserDtoComparer : IEqualityComparer +{ + public static UserDtoComparer Instance => _instance; + private static UserDtoComparer _instance = new(); + private UserDtoComparer() { } + public bool Equals(UserDto? x, UserDto? y) + { + if (x == null || y == null) return false; + return x.User.UID.Equals(y.User.UID, StringComparison.Ordinal); + } + + public int GetHashCode(UserDto obj) + { + return obj.User.UID.GetHashCode(); + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Enum/GroupPermissions.cs b/MareAPI/MareSynchronosAPI/Data/Enum/GroupPermissions.cs new file mode 100644 index 0000000..cccc712 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Enum/GroupPermissions.cs @@ -0,0 +1,11 @@ +namespace MareSynchronos.API.Data.Enum; + +[Flags] +public enum GroupPermissions +{ + NoneSet = 0x0, + DisableAnimations = 0x1, + DisableSounds = 0x2, + DisableInvites = 0x4, + DisableVFX = 0x8, +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Enum/GroupUserInfo.cs b/MareAPI/MareSynchronosAPI/Data/Enum/GroupUserInfo.cs new file mode 100644 index 0000000..ed1b3bb --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Enum/GroupUserInfo.cs @@ -0,0 +1,9 @@ +namespace MareSynchronos.API.Data.Enum; + +[Flags] +public enum GroupUserInfo +{ + None = 0x0, + IsModerator = 0x2, + IsPinned = 0x4 +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs b/MareAPI/MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs new file mode 100644 index 0000000..efa3bfd --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs @@ -0,0 +1,11 @@ +namespace MareSynchronos.API.Data.Enum; + +[Flags] +public enum GroupUserPermissions +{ + NoneSet = 0x0, + Paused = 0x1, + DisableAnimations = 0x2, + DisableSounds = 0x4, + DisableVFX = 0x8, +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Enum/MessageSeverity.cs b/MareAPI/MareSynchronosAPI/Data/Enum/MessageSeverity.cs new file mode 100644 index 0000000..b0ace02 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Enum/MessageSeverity.cs @@ -0,0 +1,8 @@ +namespace MareSynchronos.API.Data.Enum; + +public enum MessageSeverity +{ + Information, + Warning, + Error +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Enum/ObjectKind.cs b/MareAPI/MareSynchronosAPI/Data/Enum/ObjectKind.cs new file mode 100644 index 0000000..47396c4 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Enum/ObjectKind.cs @@ -0,0 +1,9 @@ +namespace MareSynchronos.API.Data.Enum; + +public enum ObjectKind +{ + Player = 0, + MinionOrMount = 1, + Companion = 2, + Pet = 3, +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Enum/UserPermissions.cs b/MareAPI/MareSynchronosAPI/Data/Enum/UserPermissions.cs new file mode 100644 index 0000000..8cc472b --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Enum/UserPermissions.cs @@ -0,0 +1,12 @@ +namespace MareSynchronos.API.Data.Enum; + +[Flags] +public enum UserPermissions +{ + NoneSet = 0, + Paired = 1, + Paused = 2, + DisableAnimations = 4, + DisableSounds = 8, + DisableVFX = 16, +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Extensions/GroupPermissionsExtensions.cs b/MareAPI/MareSynchronosAPI/Data/Extensions/GroupPermissionsExtensions.cs new file mode 100644 index 0000000..ca2236d --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Extensions/GroupPermissionsExtensions.cs @@ -0,0 +1,50 @@ +using MareSynchronos.API.Data.Enum; + +namespace MareSynchronos.API.Data.Extensions; + +public static class GroupPermissionsExtensions +{ + public static bool IsDisableAnimations(this GroupPermissions perm) + { + return perm.HasFlag(GroupPermissions.DisableAnimations); + } + + public static bool IsDisableSounds(this GroupPermissions perm) + { + return perm.HasFlag(GroupPermissions.DisableSounds); + } + + public static bool IsDisableInvites(this GroupPermissions perm) + { + return perm.HasFlag(GroupPermissions.DisableInvites); + } + + public static bool IsDisableVFX(this GroupPermissions perm) + { + return perm.HasFlag(GroupPermissions.DisableVFX); + } + + public static void SetDisableAnimations(this ref GroupPermissions perm, bool set) + { + if (set) perm |= GroupPermissions.DisableAnimations; + else perm &= ~GroupPermissions.DisableAnimations; + } + + public static void SetDisableSounds(this ref GroupPermissions perm, bool set) + { + if (set) perm |= GroupPermissions.DisableSounds; + else perm &= ~GroupPermissions.DisableSounds; + } + + public static void SetDisableInvites(this ref GroupPermissions perm, bool set) + { + if (set) perm |= GroupPermissions.DisableInvites; + else perm &= ~GroupPermissions.DisableInvites; + } + + public static void SetDisableVFX(this ref GroupPermissions perm, bool set) + { + if (set) perm |= GroupPermissions.DisableVFX; + else perm &= ~GroupPermissions.DisableVFX; + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs b/MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs new file mode 100644 index 0000000..a4608e8 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs @@ -0,0 +1,28 @@ +using MareSynchronos.API.Data.Enum; + +namespace MareSynchronos.API.Data.Extensions; + +public static class GroupUserInfoExtensions +{ + public static bool IsModerator(this GroupUserInfo info) + { + return info.HasFlag(GroupUserInfo.IsModerator); + } + + public static bool IsPinned(this GroupUserInfo info) + { + return info.HasFlag(GroupUserInfo.IsPinned); + } + + public static void SetModerator(this ref GroupUserInfo info, bool isModerator) + { + if (isModerator) info |= GroupUserInfo.IsModerator; + else info &= ~GroupUserInfo.IsModerator; + } + + public static void SetPinned(this ref GroupUserInfo info, bool isPinned) + { + if (isPinned) info |= GroupUserInfo.IsPinned; + else info &= ~GroupUserInfo.IsPinned; + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserPermissionsExtensions.cs b/MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserPermissionsExtensions.cs new file mode 100644 index 0000000..b8b2702 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Extensions/GroupUserPermissionsExtensions.cs @@ -0,0 +1,50 @@ +using MareSynchronos.API.Data.Enum; + +namespace MareSynchronos.API.Data.Extensions; + +public static class GroupUserPermissionsExtensions +{ + public static bool IsDisableAnimations(this GroupUserPermissions perm) + { + return perm.HasFlag(GroupUserPermissions.DisableAnimations); + } + + public static bool IsDisableSounds(this GroupUserPermissions perm) + { + return perm.HasFlag(GroupUserPermissions.DisableSounds); + } + + public static bool IsPaused(this GroupUserPermissions perm) + { + return perm.HasFlag(GroupUserPermissions.Paused); + } + + public static bool IsDisableVFX(this GroupUserPermissions perm) + { + return perm.HasFlag(GroupUserPermissions.DisableVFX); + } + + public static void SetDisableAnimations(this ref GroupUserPermissions perm, bool set) + { + if (set) perm |= GroupUserPermissions.DisableAnimations; + else perm &= ~GroupUserPermissions.DisableAnimations; + } + + public static void SetDisableSounds(this ref GroupUserPermissions perm, bool set) + { + if (set) perm |= GroupUserPermissions.DisableSounds; + else perm &= ~GroupUserPermissions.DisableSounds; + } + + public static void SetPaused(this ref GroupUserPermissions perm, bool set) + { + if (set) perm |= GroupUserPermissions.Paused; + else perm &= ~GroupUserPermissions.Paused; + } + + public static void SetDisableVFX(this ref GroupUserPermissions perm, bool set) + { + if (set) perm |= GroupUserPermissions.DisableVFX; + else perm &= ~GroupUserPermissions.DisableVFX; + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/Extensions/UserPermissionsExtensions.cs b/MareAPI/MareSynchronosAPI/Data/Extensions/UserPermissionsExtensions.cs new file mode 100644 index 0000000..2b80601 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/Extensions/UserPermissionsExtensions.cs @@ -0,0 +1,61 @@ +using MareSynchronos.API.Data.Enum; + +namespace MareSynchronos.API.Data.Extensions; + +public static class UserPermissionsExtensions +{ + public static bool IsPaired(this UserPermissions perm) + { + return perm.HasFlag(UserPermissions.Paired); + } + + public static bool IsPaused(this UserPermissions perm) + { + return perm.HasFlag(UserPermissions.Paused); + } + + public static bool IsDisableAnimations(this UserPermissions perm) + { + return perm.HasFlag(UserPermissions.DisableAnimations); + } + + public static bool IsDisableSounds(this UserPermissions perm) + { + return perm.HasFlag(UserPermissions.DisableSounds); + } + + public static bool IsDisableVFX(this UserPermissions perm) + { + return perm.HasFlag(UserPermissions.DisableVFX); + } + + public static void SetPaired(this ref UserPermissions perm, bool paired) + { + if (paired) perm |= UserPermissions.Paired; + else perm &= ~UserPermissions.Paired; + } + + public static void SetPaused(this ref UserPermissions perm, bool paused) + { + if (paused) perm |= UserPermissions.Paused; + else perm &= ~UserPermissions.Paused; + } + + public static void SetDisableAnimations(this ref UserPermissions perm, bool set) + { + if (set) perm |= UserPermissions.DisableAnimations; + else perm &= ~UserPermissions.DisableAnimations; + } + + public static void SetDisableSounds(this ref UserPermissions perm, bool set) + { + if (set) perm |= UserPermissions.DisableSounds; + else perm &= ~UserPermissions.DisableSounds; + } + + public static void SetDisableVFX(this ref UserPermissions perm, bool set) + { + if (set) perm |= UserPermissions.DisableVFX; + else perm &= ~UserPermissions.DisableVFX; + } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/FileReplacementData.cs b/MareAPI/MareSynchronosAPI/Data/FileReplacementData.cs new file mode 100644 index 0000000..82161a5 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/FileReplacementData.cs @@ -0,0 +1,30 @@ +using MessagePack; +using System.Text.Json.Serialization; +using System.Text.Json; +using System.Text; +using System.Security.Cryptography; + + +namespace MareSynchronos.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public class FileReplacementData +{ + public FileReplacementData() + { + DataHash = new(() => + { + var json = JsonSerializer.Serialize(this); +#pragma warning disable SYSLIB0021 // Type or member is obsolete + using SHA256CryptoServiceProvider cryptoProvider = new(); +#pragma warning restore SYSLIB0021 // Type or member is obsolete + return BitConverter.ToString(cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(json))).Replace("-", "", StringComparison.Ordinal); + }); + } + + [JsonIgnore] + public Lazy DataHash { get; } + public string[] GamePaths { get; set; } = Array.Empty(); + public string Hash { get; set; } = string.Empty; + public string FileSwapPath { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/GroupData.cs b/MareAPI/MareSynchronosAPI/Data/GroupData.cs new file mode 100644 index 0000000..877bb44 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/GroupData.cs @@ -0,0 +1,10 @@ +using MessagePack; + +namespace MareSynchronos.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupData(string GID, string? Alias = null) +{ + [IgnoreMember] + public string AliasOrGID => string.IsNullOrWhiteSpace(Alias) ? GID : Alias; +} diff --git a/MareAPI/MareSynchronosAPI/Data/SignedChatMessage.cs b/MareAPI/MareSynchronosAPI/Data/SignedChatMessage.cs new file mode 100644 index 0000000..edfd8cc --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/SignedChatMessage.cs @@ -0,0 +1,14 @@ +using MessagePack; + +namespace MareSynchronos.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public record SignedChatMessage(ChatMessage Message, UserData Sender) : ChatMessage(Message) +{ + // Sender and timestamp are set by the server + public UserData Sender { get; set; } = Sender; + public long Timestamp { get; set; } = 0; + // Signature is generated by the server as SHA256(Sender.UID | Timestamp | Destination | Message) + // Where Destination is either the receiver's UID, or the group GID + public string Signature { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Data/UserData.cs b/MareAPI/MareSynchronosAPI/Data/UserData.cs new file mode 100644 index 0000000..3bc74cf --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Data/UserData.cs @@ -0,0 +1,10 @@ +using MessagePack; + +namespace MareSynchronos.API.Data; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserData(string UID, string? Alias = null) +{ + [IgnoreMember] + public string AliasOrUID => string.IsNullOrWhiteSpace(Alias) ? UID : Alias; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs b/MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs new file mode 100644 index 0000000..ce3f741 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs @@ -0,0 +1,12 @@ +using MessagePack; + +namespace MareSynchronos.API.Dto.Account; + +[MessagePackObject(keyAsPropertyName: true)] +public record RegisterReplyDto +{ + public bool Success { get; set; } = false; + public string ErrorMessage { get; set; } = string.Empty; + public string UID { get; set; } = string.Empty; + public string SecretKey { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs b/MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs new file mode 100644 index 0000000..59f7fe5 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs @@ -0,0 +1,11 @@ +using MessagePack; + +namespace MareSynchronos.API.Dto.Account; + +[MessagePackObject(keyAsPropertyName: true)] +public record RegisterReplyV2Dto +{ + public bool Success { get; set; } = false; + public string ErrorMessage { get; set; } = string.Empty; + public string UID { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs b/MareAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs new file mode 100644 index 0000000..d3033fd --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs @@ -0,0 +1,11 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto; + +[MessagePackObject(keyAsPropertyName: true)] +public record AuthReplyDto +{ + public string Token { get; set; } = string.Empty; + public string? WellKnown { get; set; } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs new file mode 100644 index 0000000..9c53eaa --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs @@ -0,0 +1,9 @@ +namespace MareSynchronos.API.Dto.CharaData; + +public enum AccessTypeDto +{ + Individuals, + ClosePairs, + AllPairs, + Public +} diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs new file mode 100644 index 0000000..5d450b8 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs @@ -0,0 +1,14 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.CharaData; + +[MessagePackObject(keyAsPropertyName: true)] +public record CharaDataDownloadDto(string Id, UserData Uploader) : CharaDataDto(Id, Uploader) +{ + public string GlamourerData { get; init; } = string.Empty; + public string CustomizeData { get; init; } = string.Empty; + public string ManipulationData { get; set; } = string.Empty; + public List FileGamePaths { get; init; } = []; + public List FileSwaps { get; init; } = []; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs new file mode 100644 index 0000000..dbf4a26 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs @@ -0,0 +1,9 @@ +using MareSynchronos.API.Data; + +namespace MareSynchronos.API.Dto.CharaData; + +public record CharaDataDto(string Id, UserData Uploader) +{ + public string Description { get; init; } = string.Empty; + public DateTime UpdatedDate { get; init; } +} diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs new file mode 100644 index 0000000..d8b4016 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs @@ -0,0 +1,88 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.CharaData; + +[MessagePackObject(keyAsPropertyName: true)] +public record CharaDataFullDto(string Id, UserData Uploader) : CharaDataDto(Id, Uploader) +{ + public DateTime CreatedDate { get; init; } + public DateTime ExpiryDate { get; set; } + public string GlamourerData { get; set; } = string.Empty; + public string CustomizeData { get; set; } = string.Empty; + public string ManipulationData { get; set; } = string.Empty; + public int DownloadCount { get; set; } = 0; + public List AllowedUsers { get; set; } = []; + public List AllowedGroups { get; set; } = []; + public List FileGamePaths { get; set; } = []; + public List FileSwaps { get; set; } = []; + public List OriginalFiles { get; set; } = []; + public AccessTypeDto AccessType { get; set; } + public ShareTypeDto ShareType { get; set; } + public List PoseData { get; set; } = []; +} + +[MessagePackObject(keyAsPropertyName: true)] +public record GamePathEntry(string HashOrFileSwap, string GamePath); + +[MessagePackObject(keyAsPropertyName: true)] +public record PoseEntry(long? Id) +{ + public string? Description { get; set; } = string.Empty; + public string? PoseData { get; set; } = string.Empty; + public WorldData? WorldData { get; set; } +} + +[MessagePackObject] +public record struct WorldData +{ + [Key(0)] public LocationInfo LocationInfo { get; set; } + [Key(1)] public float PositionX { get; set; } + [Key(2)] public float PositionY { get; set; } + [Key(3)] public float PositionZ { get; set; } + [Key(4)] public float RotationX { get; set; } + [Key(5)] public float RotationY { get; set; } + [Key(6)] public float RotationZ { get; set; } + [Key(7)] public float RotationW { get; set; } + [Key(8)] public float ScaleX { get; set; } + [Key(9)] public float ScaleY { get; set; } + [Key(10)] public float ScaleZ { get; set; } +} + +[MessagePackObject] +public record struct LocationInfo +{ + [Key(0)] public uint ServerId { get; set; } + [Key(1)] public uint MapId { get; set; } + [Key(2)] public uint TerritoryId { get; set; } + [Key(3)] public uint DivisionId { get; set; } + [Key(4)] public uint WardId { get; set; } + [Key(5)] public uint HouseId { get; set; } + [Key(6)] public uint RoomId { get; set; } +} + +[MessagePackObject] +public record struct PoseData +{ + [Key(0)] public bool IsDelta { get; set; } + [Key(1)] public Dictionary Bones { get; set; } + [Key(2)] public Dictionary MainHand { get; set; } + [Key(3)] public Dictionary OffHand { get; set; } + [Key(4)] public BoneData ModelDifference { get; set; } +} + +[MessagePackObject] +public record struct BoneData +{ + [Key(0)] public bool Exists { get; set; } + [Key(1)] public float PositionX { get; set; } + [Key(2)] public float PositionY { get; set; } + [Key(3)] public float PositionZ { get; set; } + [Key(4)] public float RotationX { get; set; } + [Key(5)] public float RotationY { get; set; } + [Key(6)] public float RotationZ { get; set; } + [Key(7)] public float RotationW { get; set; } + [Key(8)] public float ScaleX { get; set; } + [Key(9)] public float ScaleY { get; set; } + [Key(10)] public float ScaleZ { get; set; } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs new file mode 100644 index 0000000..7afb6b2 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs @@ -0,0 +1,11 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.CharaData; + +[MessagePackObject(keyAsPropertyName: true)] +public record CharaDataMetaInfoDto(string Id, UserData Uploader) : CharaDataDto(Id, Uploader) +{ + public bool CanBeDownloaded { get; init; } + public List PoseData { get; set; } = []; +} diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs new file mode 100644 index 0000000..30d1348 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs @@ -0,0 +1,20 @@ +using MessagePack; + +namespace MareSynchronos.API.Dto.CharaData; + +[MessagePackObject(keyAsPropertyName: true)] +public record CharaDataUpdateDto(string Id) +{ + public string? Description { get; set; } + public DateTime? ExpiryDate { get; set; } + public string? GlamourerData { get; set; } + public string? CustomizeData { get; set; } + public string? ManipulationData { get; set; } + public List? AllowedUsers { get; set; } + public List? AllowedGroups { get; set; } + public List? FileGamePaths { get; set; } + public List? FileSwaps { get; set; } + public AccessTypeDto? AccessType { get; set; } + public ShareTypeDto? ShareType { get; set; } + public List? Poses { get; set; } +} diff --git a/MareAPI/MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs b/MareAPI/MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs new file mode 100644 index 0000000..ed55f94 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs @@ -0,0 +1,7 @@ +namespace MareSynchronos.API.Dto.CharaData; + +public enum ShareTypeDto +{ + Private, + Shared +} diff --git a/MareAPI/MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs b/MareAPI/MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs new file mode 100644 index 0000000..c946c00 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs @@ -0,0 +1,13 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; +using MessagePack; + +namespace MareSynchronos.API.Dto.Chat; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupChatMsgDto(GroupDto Group, SignedChatMessage Message) +{ + public GroupDto Group = Group; + public SignedChatMessage Message = Message; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs b/MareAPI/MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs new file mode 100644 index 0000000..d82855b --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs @@ -0,0 +1,11 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.User; +using MessagePack; + +namespace MareSynchronos.API.Dto.Chat; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserChatMsgDto(SignedChatMessage Message) +{ + public SignedChatMessage Message = Message; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/ConnectionDto.cs b/MareAPI/MareSynchronosAPI/Dto/ConnectionDto.cs new file mode 100644 index 0000000..04c818e --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/ConnectionDto.cs @@ -0,0 +1,25 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto; + +[MessagePackObject(keyAsPropertyName: true)] +public record ConnectionDto(UserData User) +{ + public Version CurrentClientVersion { get; set; } = new(0, 0, 0); + public int ServerVersion { get; set; } + public bool IsAdmin { get; set; } + public bool IsModerator { get; set; } + public ServerInfo ServerInfo { get; set; } = new(); +} + +[MessagePackObject(keyAsPropertyName: true)] +public record ServerInfo +{ + public string ShardName { get; set; } = string.Empty; + public int MaxGroupUserCount { get; set; } + public int MaxGroupsCreatedByUser { get; set; } + public int MaxGroupsJoinedByUser { get; set; } + public Uri FileServerAddress { get; set; } = new Uri("http://nonemptyuri"); + public int MaxCharaData { get; set; } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Files/DownloadFileDto.cs b/MareAPI/MareSynchronosAPI/Dto/Files/DownloadFileDto.cs new file mode 100644 index 0000000..d2ffe05 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Files/DownloadFileDto.cs @@ -0,0 +1,14 @@ +using MessagePack; + +namespace MareSynchronos.API.Dto.Files; + +[MessagePackObject(keyAsPropertyName: true)] +public record DownloadFileDto : ITransferFileDto +{ + public bool FileExists { get; set; } = true; + public string Hash { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + public long Size { get; set; } = 0; + public bool IsForbidden { get; set; } = false; + public string ForbiddenBy { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Files/FilesSendDto.cs b/MareAPI/MareSynchronosAPI/Dto/Files/FilesSendDto.cs new file mode 100644 index 0000000..b7a6735 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Files/FilesSendDto.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MareSynchronos.API.Dto.Files; + +public class FilesSendDto +{ + public List FileHashes { get; set; } = new(); + public List UIDs { get; set; } = new(); +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Files/ITransferFileDto.cs b/MareAPI/MareSynchronosAPI/Dto/Files/ITransferFileDto.cs new file mode 100644 index 0000000..fb20e5a --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Files/ITransferFileDto.cs @@ -0,0 +1,8 @@ +namespace MareSynchronos.API.Dto.Files; + +public interface ITransferFileDto +{ + string Hash { get; set; } + bool IsForbidden { get; set; } + string ForbiddenBy { get; set; } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Files/UploadFileDto.cs b/MareAPI/MareSynchronosAPI/Dto/Files/UploadFileDto.cs new file mode 100644 index 0000000..f10b27d --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Files/UploadFileDto.cs @@ -0,0 +1,11 @@ +using MessagePack; + +namespace MareSynchronos.API.Dto.Files; + +[MessagePackObject(keyAsPropertyName: true)] +public record UploadFileDto : ITransferFileDto +{ + public string Hash { get; set; } = string.Empty; + public bool IsForbidden { get; set; } = false; + public string ForbiddenBy { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs new file mode 100644 index 0000000..36ed1f9 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs @@ -0,0 +1,19 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record BannedGroupUserDto : GroupPairDto +{ + public BannedGroupUserDto(GroupData group, UserData user, string reason, DateTime bannedOn, string bannedBy) : base(group, user) + { + Reason = reason; + BannedOn = bannedOn; + BannedBy = bannedBy; + } + + public string Reason { get; set; } + public DateTime BannedOn { get; set; } + public string BannedBy { get; set; } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupDto.cs new file mode 100644 index 0000000..5b5b71a --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupDto.cs @@ -0,0 +1,13 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupDto(GroupData Group) +{ + public GroupData Group { get; set; } = Group; + public string GID => Group.GID; + public string? GroupAlias => Group.Alias; + public string GroupAliasOrGID => Group.AliasOrGID; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs new file mode 100644 index 0000000..0591293 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs @@ -0,0 +1,12 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupFullInfoDto(GroupData Group, UserData Owner, GroupPermissions GroupPermissions, GroupUserPermissions GroupUserPermissions, GroupUserInfo GroupUserInfo) : GroupInfoDto(Group, Owner, GroupPermissions) +{ + public GroupUserPermissions GroupUserPermissions { get; set; } = GroupUserPermissions; + public GroupUserInfo GroupUserInfo { get; set; } = GroupUserInfo; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupInfoDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupInfoDto.cs new file mode 100644 index 0000000..193072b --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupInfoDto.cs @@ -0,0 +1,16 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupInfoDto(GroupData Group, UserData Owner, GroupPermissions GroupPermissions) : GroupDto(Group) +{ + public GroupPermissions GroupPermissions { get; set; } = GroupPermissions; + public UserData Owner { get; set; } = Owner; + + public string OwnerUID => Owner.UID; + public string? OwnerAlias => Owner.Alias; + public string OwnerAliasOrUID => Owner.AliasOrUID; +} diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairDto.cs new file mode 100644 index 0000000..c2e748d --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairDto.cs @@ -0,0 +1,12 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPairDto(GroupData Group, UserData User) : GroupDto(Group) +{ + public string UID => User.UID; + public string? UserAlias => User.Alias; + public string UserAliasOrUID => User.AliasOrUID; +} diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs new file mode 100644 index 0000000..5a594df --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs @@ -0,0 +1,12 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPairFullInfoDto(GroupData Group, UserData User, GroupUserInfo GroupPairStatusInfo, GroupUserPermissions GroupUserPermissions) : GroupPairDto(Group, User) +{ + public GroupUserInfo GroupPairStatusInfo { get; set; } = GroupPairStatusInfo; + public GroupUserPermissions GroupUserPermissions { get; set; } = GroupUserPermissions; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs new file mode 100644 index 0000000..8a37f68 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs @@ -0,0 +1,8 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPairUserInfoDto(GroupData Group, UserData User, GroupUserInfo GroupUserInfo) : GroupPairDto(Group, User); diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserPermissionDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserPermissionDto.cs new file mode 100644 index 0000000..d1f152f --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPairUserPermissionDto.cs @@ -0,0 +1,8 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPairUserPermissionDto(GroupData Group, UserData User, GroupUserPermissions GroupPairPermissions) : GroupPairDto(Group, User); diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs new file mode 100644 index 0000000..bcc31f0 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPasswordDto(GroupData Group, string Password) : GroupDto(Group); diff --git a/MareAPI/MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs new file mode 100644 index 0000000..70dbf80 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs @@ -0,0 +1,8 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.Group; + +[MessagePackObject(keyAsPropertyName: true)] +public record GroupPermissionDto(GroupData Group, GroupPermissions Permissions) : GroupDto(Group); diff --git a/MareAPI/MareSynchronosAPI/Dto/SystemInfoDto.cs b/MareAPI/MareSynchronosAPI/Dto/SystemInfoDto.cs new file mode 100644 index 0000000..eb84f1a --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/SystemInfoDto.cs @@ -0,0 +1,9 @@ +using MessagePack; + +namespace MareSynchronos.API.Dto; + +[MessagePackObject(keyAsPropertyName: true)] +public record SystemInfoDto +{ + public int OnlineUsers { get; set; } +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs new file mode 100644 index 0000000..a4233d5 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record OnlineUserCharaDataDto(UserData User, CharacterData CharaData) : UserDto(User); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs new file mode 100644 index 0000000..dbc7129 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record OnlineUserIdentDto(UserData User, string Ident) : UserDto(User); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs new file mode 100644 index 0000000..1b33590 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserCharaDataMessageDto(List Recipients, CharacterData CharaData); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/UserDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/UserDto.cs new file mode 100644 index 0000000..ce105bf --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/UserDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserDto(UserData User); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/UserPairDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/UserPairDto.cs new file mode 100644 index 0000000..3d92ad6 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/UserPairDto.cs @@ -0,0 +1,12 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserPairDto(UserData User, UserPermissions OwnPermissions, UserPermissions OtherPermissions) : UserDto(User) +{ + public UserPermissions OwnPermissions { get; set; } = OwnPermissions; + public UserPermissions OtherPermissions { get; set; } = OtherPermissions; +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/UserPermissionsDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/UserPermissionsDto.cs new file mode 100644 index 0000000..772040b --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/UserPermissionsDto.cs @@ -0,0 +1,8 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserPermissionsDto(UserData User, UserPermissions Permissions) : UserDto(User); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/UserProfileDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/UserProfileDto.cs new file mode 100644 index 0000000..0b103e5 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/UserProfileDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserProfileDto(UserData User, bool Disabled, bool? IsNSFW, string? ProfilePictureBase64, string? Description) : UserDto(User); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Dto/User/UserProfileReportDto.cs b/MareAPI/MareSynchronosAPI/Dto/User/UserProfileReportDto.cs new file mode 100644 index 0000000..02ed9ef --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Dto/User/UserProfileReportDto.cs @@ -0,0 +1,7 @@ +using MareSynchronos.API.Data; +using MessagePack; + +namespace MareSynchronos.API.Dto.User; + +[MessagePackObject(keyAsPropertyName: true)] +public record UserProfileReportDto(UserData User, string ProfileReport) : UserDto(User); \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/MareSynchronos.API.csproj b/MareAPI/MareSynchronosAPI/MareSynchronos.API.csproj new file mode 100644 index 0000000..44e5fc8 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/MareSynchronos.API.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/MareAPI/MareSynchronosAPI/MareSynchronosAPI.sln b/MareAPI/MareSynchronosAPI/MareSynchronosAPI.sln new file mode 100644 index 0000000..ffde134 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/MareSynchronosAPI.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32602.215 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "MareSynchronos.API.csproj", "{CD05EE19-802F-4490-AAD8-CAD4BF1D630D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CD05EE19-802F-4490-AAD8-CAD4BF1D630D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD05EE19-802F-4490-AAD8-CAD4BF1D630D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD05EE19-802F-4490-AAD8-CAD4BF1D630D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD05EE19-802F-4490-AAD8-CAD4BF1D630D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DFB70C71-AB27-468D-A08B-218CA79BF69D} + EndGlobalSection +EndGlobal diff --git a/MareAPI/MareSynchronosAPI/Routes/MareAuth.cs b/MareAPI/MareSynchronosAPI/Routes/MareAuth.cs new file mode 100644 index 0000000..2bef31e --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Routes/MareAuth.cs @@ -0,0 +1,14 @@ +namespace MareSynchronos.API.Routes; + +public class MareAuth +{ + public const string Auth = "/auth"; + public const string Auth_CreateIdent = "createWithIdent"; + public const string Auth_CreateIdentV2 = "createWithIdentV2"; + public const string Auth_Register = "registerNewKey"; + public const string Auth_RegisterV2 = "registerNewKeyV2"; + public static Uri AuthFullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_CreateIdent); + public static Uri AuthV2FullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_CreateIdentV2); + public static Uri AuthRegisterFullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_Register); + public static Uri AuthRegisterV2FullPath(Uri baseUri) => new Uri(baseUri, Auth + "/" + Auth_RegisterV2); +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/Routes/MareFiles.cs b/MareAPI/MareSynchronosAPI/Routes/MareFiles.cs new file mode 100644 index 0000000..a4e5f5d --- /dev/null +++ b/MareAPI/MareSynchronosAPI/Routes/MareFiles.cs @@ -0,0 +1,45 @@ +namespace MareSynchronos.API.Routes; + +public class MareFiles +{ + public const string Cache = "/cache"; + public const string Cache_Get = "get"; + + public const string Request = "/request"; + public const string Request_Cancel = "cancel"; + public const string Request_Check = "check"; + public const string Request_Enqueue = "enqueue"; + public const string Request_RequestFile = "file"; + + public const string ServerFiles = "/files"; + public const string ServerFiles_DeleteAll = "deleteAll"; + public const string ServerFiles_FilesSend = "filesSend"; + public const string ServerFiles_GetSizes = "getFileSizes"; + public const string ServerFiles_Upload = "upload"; + public const string ServerFiles_UploadRaw = "uploadRaw"; + public const string ServerFiles_UploadMunged = "uploadMunged"; + + public const string Distribution = "/dist"; + public const string Distribution_Get = "get"; + + public const string Main = "/main"; + public const string Main_SendReady = "sendReady"; + + public static Uri CacheGetFullPath(Uri baseUri, Guid requestId) => new(baseUri, Cache + "/" + Cache_Get + "?requestId=" + requestId.ToString()); + + public static Uri RequestCancelFullPath(Uri baseUri, Guid guid) => new Uri(baseUri, Request + "/" + Request_Cancel + "?requestId=" + guid.ToString()); + public static Uri RequestCheckQueueFullPath(Uri baseUri, Guid guid) => new Uri(baseUri, Request + "/" + Request_Check + "?requestId=" + guid.ToString()); + public static Uri RequestEnqueueFullPath(Uri baseUri) => new(baseUri, Request + "/" + Request_Enqueue); + public static Uri RequestRequestFileFullPath(Uri baseUri, string hash) => new(baseUri, Request + "/" + Request_RequestFile + "?file=" + hash); + + public static Uri ServerFilesDeleteAllFullPath(Uri baseUri) => new(baseUri, ServerFiles + "/" + ServerFiles_DeleteAll); + public static Uri ServerFilesFilesSendFullPath(Uri baseUri) => new(baseUri, ServerFiles + "/" + ServerFiles_FilesSend); + public static Uri ServerFilesGetSizesFullPath(Uri baseUri) => new(baseUri, ServerFiles + "/" + ServerFiles_GetSizes); + public static Uri ServerFilesUploadFullPath(Uri baseUri, string hash) => new(baseUri, ServerFiles + "/" + ServerFiles_Upload + "/" + hash); + public static Uri ServerFilesUploadRawFullPath(Uri baseUri, string hash) => new(baseUri, ServerFiles + "/" + ServerFiles_UploadRaw + "/" + hash); + public static Uri ServerFilesUploadMunged(Uri baseUri, string hash) => new(baseUri, ServerFiles + "/" + ServerFiles_UploadMunged + "/" + hash); + + public static Uri DistributionGetFullPath(Uri baseUri, string hash) => new(baseUri, Distribution + "/" + Distribution_Get + "?file=" + hash); + + public static Uri MainSendReadyFullPath(Uri baseUri, string uid, Guid request) => new(baseUri, Main + "/" + Main_SendReady + "/" + "?uid=" + uid + "&requestId=" + request.ToString()); +} \ No newline at end of file diff --git a/MareAPI/MareSynchronosAPI/SignalR/IMareHub.cs b/MareAPI/MareSynchronosAPI/SignalR/IMareHub.cs new file mode 100644 index 0000000..7475116 --- /dev/null +++ b/MareAPI/MareSynchronosAPI/SignalR/IMareHub.cs @@ -0,0 +1,144 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.API.Dto.Chat; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; + +namespace MareSynchronos.API.SignalR; + +public interface IMareHub +{ + const int ApiVersion = 1026; + const string Path = "/mare"; + + Task CheckClientHealth(); + + Task Client_DownloadReady(Guid requestId); + + Task Client_GroupChangePermissions(GroupPermissionDto groupPermission); + + Task Client_GroupChatMsg(GroupChatMsgDto groupChatMsgDto); + + Task Client_GroupDelete(GroupDto groupDto); + + Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto permissionDto); + + Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto userInfo); + + Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto); + + Task Client_GroupPairLeft(GroupPairDto groupPairDto); + + Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo); + + Task Client_GroupSendInfo(GroupInfoDto groupInfo); + + Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message); + + Task Client_UpdateSystemInfo(SystemInfoDto systemInfo); + + Task Client_UserAddClientPair(UserPairDto dto); + + Task Client_UserChatMsg(UserChatMsgDto chatMsgDto); + + Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto); + + Task Client_UserReceiveUploadStatus(UserDto dto); + + Task Client_UserRemoveClientPair(UserDto dto); + + Task Client_UserSendOffline(UserDto dto); + + Task Client_UserSendOnline(OnlineUserIdentDto dto); + + Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto); + + Task Client_UserUpdateProfile(UserDto dto); + + Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto); + + Task Client_GposeLobbyJoin(UserData userData); + Task Client_GposeLobbyLeave(UserData userData); + Task Client_GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto); + Task Client_GposeLobbyPushPoseData(UserData userData, PoseData poseData); + Task Client_GposeLobbyPushWorldData(UserData userData, WorldData worldData); + + Task GetConnectionDto(); + + Task GroupBanUser(GroupPairDto dto, string reason); + + Task GroupChangeGroupPermissionState(GroupPermissionDto dto); + + Task GroupChangeIndividualPermissionState(GroupPairUserPermissionDto dto); + + Task GroupChangeOwnership(GroupPairDto groupPair); + + Task GroupChangePassword(GroupPasswordDto groupPassword); + + Task GroupChatSendMsg(GroupDto group, ChatMessage message); + + Task GroupClear(GroupDto group); + + Task GroupCreate(); + + Task> GroupCreateTempInvite(GroupDto group, int amount); + + Task GroupDelete(GroupDto group); + + Task> GroupGetBannedUsers(GroupDto group); + + Task GroupJoin(GroupPasswordDto passwordedGroup); + + Task GroupLeave(GroupDto group); + + Task GroupRemoveUser(GroupPairDto groupPair); + + Task GroupSetUserInfo(GroupPairUserInfoDto groupPair); + + Task> GroupsGetAll(); + + Task> GroupsGetUsersInGroup(GroupDto group); + + Task GroupUnbanUser(GroupPairDto groupPair); + Task GroupPrune(GroupDto group, int days, bool execute); + + Task UserAddPair(UserDto user); + + Task UserChatSendMsg(UserDto user, ChatMessage message); + + Task UserDelete(); + + Task> UserGetOnlinePairs(); + + Task> UserGetPairedClients(); + + Task UserGetProfile(UserDto dto); + + Task UserPushData(UserCharaDataMessageDto dto); + + Task UserRemovePair(UserDto userDto); + + Task UserReportProfile(UserProfileReportDto userDto); + + Task UserSetPairPermissions(UserPermissionsDto userPermissions); + + Task UserSetProfile(UserProfileDto userDescription); + + Task CharaDataCreate(); + Task CharaDataUpdate(CharaDataUpdateDto updateDto); + Task CharaDataDelete(string id); + Task CharaDataGetMetainfo(string id); + Task CharaDataDownload(string id); + Task> CharaDataGetOwn(); + Task> CharaDataGetShared(); + Task CharaDataAttemptRestore(string id); + + Task GposeLobbyCreate(); + Task> GposeLobbyJoin(string lobbyId); + Task GposeLobbyLeave(); + Task GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto); + Task GposeLobbyPushPoseData(PoseData poseData); + Task GposeLobbyPushWorldData(WorldData worldData); +} diff --git a/MareAPI/MareSynchronosAPI/SignalR/IMareHubClient.cs b/MareAPI/MareSynchronosAPI/SignalR/IMareHubClient.cs new file mode 100644 index 0000000..d13cbaa --- /dev/null +++ b/MareAPI/MareSynchronosAPI/SignalR/IMareHubClient.cs @@ -0,0 +1,62 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.API.Dto.Chat; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; + +namespace MareSynchronos.API.SignalR; + +public interface IMareHubClient : IMareHub +{ + void OnDownloadReady(Action act); + + void OnGroupChangePermissions(Action act); + + void OnGroupChatMsg(Action groupChatMsgDto); + + void OnGroupDelete(Action act); + + void OnGroupPairChangePermissions(Action act); + + void OnGroupPairChangeUserInfo(Action act); + + void OnGroupPairJoined(Action act); + + void OnGroupPairLeft(Action act); + + void OnGroupSendFullInfo(Action act); + + void OnGroupSendInfo(Action act); + + void OnReceiveServerMessage(Action act); + + void OnUpdateSystemInfo(Action act); + + void OnUserAddClientPair(Action act); + + void OnUserChatMsg(Action chatMsgDto); + + void OnUserReceiveCharacterData(Action act); + + void OnUserReceiveUploadStatus(Action act); + + void OnUserRemoveClientPair(Action act); + + void OnUserSendOffline(Action act); + + void OnUserSendOnline(Action act); + + void OnUserUpdateOtherPairPermissions(Action act); + + void OnUserUpdateProfile(Action act); + + void OnUserUpdateSelfPairPermissions(Action act); + + void OnGposeLobbyJoin(Action act); + void OnGposeLobbyLeave(Action act); + void OnGposeLobbyPushCharacterData(Action act); + void OnGposeLobbyPushPoseData(Action act); + void OnGposeLobbyPushWorldData(Action act); +} \ No newline at end of file diff --git a/MareSynchronos.sln b/MareSynchronos.sln new file mode 100644 index 0000000..ec5ad79 --- /dev/null +++ b/MareSynchronos.sln @@ -0,0 +1,46 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32328.378 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos", "MareSynchronos\MareSynchronos.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj", "{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{585B740D-BA2C-429B-9CF3-B2D223423748}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 + {5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Debug|x64.Build.0 = Debug|Any CPU + {5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Release|Any CPU.Build.0 = Release|Any CPU + {5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Release|x64.ActiveCfg = Release|Any CPU + {5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}.Release|x64.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} + EndGlobalSection +EndGlobal diff --git a/MareSynchronos/.editorconfig b/MareSynchronos/.editorconfig new file mode 100644 index 0000000..77dfdf8 --- /dev/null +++ b/MareSynchronos/.editorconfig @@ -0,0 +1,120 @@ + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion + +[*.cs] +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +dotnet_diagnostic.MA0076.severity = silent +dotnet_diagnostic.MA0051.severity = silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +dotnet_diagnostic.S1075.severity = silent +dotnet_diagnostic.SS3358.severity = suggestion +dotnet_diagnostic.MA0007.severity = silent +dotnet_diagnostic.MA0075.severity = silent + +# S3358: Ternary operators should not be nested +dotnet_diagnostic.S3358.severity = suggestion + +# S6678: Use PascalCase for named placeholders +dotnet_diagnostic.S6678.severity = none + +# S6605: Collection-specific "Exists" method should be used instead of the "Any" extension +dotnet_diagnostic.S6605.severity = none + +# S6667: Logging in a catch clause should pass the caught exception as a parameter. +dotnet_diagnostic.S6667.severity = suggestion + +# IDE0290: Use primary constructor +csharp_style_prefer_primary_constructors = false + +# S3267: Loops should be simplified with "LINQ" expressions +dotnet_diagnostic.S3267.severity = silent + +# MA0048: File name must match type name +dotnet_diagnostic.MA0048.severity = silent diff --git a/MareSynchronos/FileCache/CacheMonitor.cs b/MareSynchronos/FileCache/CacheMonitor.cs new file mode 100644 index 0000000..483fa0c --- /dev/null +++ b/MareSynchronos/FileCache/CacheMonitor.cs @@ -0,0 +1,859 @@ +using MareSynchronos.Interop.Ipc; +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; + +namespace MareSynchronos.FileCache; + +public sealed class CacheMonitor : DisposableMediatorSubscriberBase +{ + private readonly MareConfigService _configService; + private readonly DalamudUtilService _dalamudUtil; + private readonly FileCompactor _fileCompactor; + private readonly FileCacheManager _fileDbManager; + private readonly IpcManager _ipcManager; + private readonly PerformanceCollectorService _performanceCollector; + private long _currentFileProgress = 0; + 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"]; + + public CacheMonitor(ILogger logger, IpcManager ipcManager, MareConfigService configService, + FileCacheManager fileDbManager, MareMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil, + FileCompactor fileCompactor) : base(logger, mediator) + { + _ipcManager = ipcManager; + _configService = configService; + _fileDbManager = fileDbManager; + _performanceCollector = performanceCollector; + _dalamudUtil = dalamudUtil; + _fileCompactor = fileCompactor; + Mediator.Subscribe(this, (_) => + { + StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory); + StartMareWatcher(configService.Current.CacheFolder); + StartSubstWatcher(_fileDbManager.SubstFolder); + InvokeScan(); + }); + Mediator.Subscribe(this, (msg) => HaltScan(msg.Source)); + Mediator.Subscribe(this, (msg) => ResumeScan(msg.Source)); + Mediator.Subscribe(this, (_) => + { + StartMareWatcher(configService.Current.CacheFolder); + StartSubstWatcher(_fileDbManager.SubstFolder); + StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory); + InvokeScan(); + }); + Mediator.Subscribe(this, (msg) => + { + StartPenumbraWatcher(msg.ModDirectory); + InvokeScan(); + }); + if (_ipcManager.Penumbra.APIAvailable && !string.IsNullOrEmpty(_ipcManager.Penumbra.ModDirectory)) + { + StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory); + } + if (configService.Current.HasValidSetup()) + { + StartMareWatcher(configService.Current.CacheFolder); + StartSubstWatcher(_fileDbManager.SubstFolder); + InvokeScan(); + } + + var token = _periodicCalculationTokenSource.Token; + _ = Task.Run(async () => + { + Logger.LogInformation("Starting Periodic Storage Directory Calculation Task"); + var token = _periodicCalculationTokenSource.Token; + while (!token.IsCancellationRequested) + { + try + { + while (_dalamudUtil.IsOnFrameworkThread && !token.IsCancellationRequested) + { + await Task.Delay(1).ConfigureAwait(false); + } + + RecalculateFileCacheSize(token); + } + catch + { + // ignore + } + await Task.Delay(TimeSpan.FromMinutes(1), token).ConfigureAwait(false); + } + }, token); + } + + public long CurrentFileProgress => _currentFileProgress; + public long FileCacheSize { get; set; } + public long FileCacheDriveFree { get; set; } + public ConcurrentDictionary> HaltScanLocks { get; set; } = new(StringComparer.Ordinal); + public bool IsScanRunning => CurrentFileProgress > 0 || TotalFiles > 0; + public long TotalFiles { get; private set; } + public long TotalFilesStorage { get; private set; } + + public void HaltScan(string source) + { + HaltScanLocks.TryAdd(source, new(0)); + Interlocked.Increment(ref HaltScanLocks[source].Value); + } + + record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null); + private readonly Dictionary _watcherChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _mareChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _substChanges = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public void StopMonitoring() + { + Logger.LogInformation("Stopping monitoring of Penumbra and Mare storage folders"); + MareWatcher?.Dispose(); + SubstWatcher?.Dispose(); + PenumbraWatcher?.Dispose(); + MareWatcher = null; + SubstWatcher = null; + PenumbraWatcher = null; + } + + public bool StorageisNTFS { get; private set; } = false; + + public void StartMareWatcher(string? snowPath) + { + MareWatcher?.Dispose(); + if (string.IsNullOrEmpty(snowPath) || !Directory.Exists(snowPath)) + { + MareWatcher = null; + Logger.LogWarning("Snowcloak file path is not set, cannot start the FSW for Snowcloak."); + return; + } + + DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName); + StorageisNTFS = string.Equals("NTFS", di.DriveFormat, StringComparison.OrdinalIgnoreCase); + Logger.LogInformation("Snowcloak Storage is on NTFS drive: {isNtfs}", StorageisNTFS); + + Logger.LogDebug("Initializing Mare FSW on {path}", snowPath); + MareWatcher = new() + { + Path = snowPath, + InternalBufferSize = 8388608, + NotifyFilter = NotifyFilters.CreationTime + | NotifyFilters.LastWrite + | NotifyFilters.FileName + | NotifyFilters.DirectoryName + | NotifyFilters.Size, + Filter = "*.*", + IncludeSubdirectories = false, + }; + + MareWatcher.Deleted += MareWatcher_FileChanged; + MareWatcher.Created += MareWatcher_FileChanged; + MareWatcher.EnableRaisingEvents = true; + } + + public void StartSubstWatcher(string? substPath) + { + SubstWatcher?.Dispose(); + if (string.IsNullOrEmpty(substPath)) + { + SubstWatcher = null; + Logger.LogWarning("Snowcloak file path is not set, cannot start the FSW for Snowcloak."); + return; + } + + try + { + if (!Directory.Exists(substPath)) + Directory.CreateDirectory(substPath); + } + catch + { + Logger.LogWarning("Could not create subst directory at {path}.", substPath); + return; + } + + Logger.LogDebug("Initializing Subst FSW on {path}", substPath); + SubstWatcher = new() + { + Path = substPath, + InternalBufferSize = 8388608, + NotifyFilter = NotifyFilters.CreationTime + | NotifyFilters.LastWrite + | NotifyFilters.FileName + | NotifyFilters.DirectoryName + | NotifyFilters.Size, + Filter = "*.*", + IncludeSubdirectories = false, + }; + + SubstWatcher.Deleted += SubstWatcher_FileChanged; + SubstWatcher.Created += SubstWatcher_FileChanged; + SubstWatcher.EnableRaisingEvents = true; + } + + private void MareWatcher_FileChanged(object sender, FileSystemEventArgs e) + { + Logger.LogTrace("Snowcloak FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath); + + if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; + + lock (_mareChanges) + { + _mareChanges[e.FullPath] = new(e.ChangeType); + } + + _ = MareWatcherExecution(); + } + + private void SubstWatcher_FileChanged(object sender, FileSystemEventArgs e) + { + Logger.LogTrace("Subst FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath); + + if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; + + lock (_substChanges) + { + _substChanges[e.FullPath] = new(e.ChangeType); + } + + _ = SubstWatcherExecution(); + } + + public void StartPenumbraWatcher(string? penumbraPath) + { + PenumbraWatcher?.Dispose(); + if (string.IsNullOrEmpty(penumbraPath)) + { + PenumbraWatcher = null; + Logger.LogWarning("Penumbra is not connected or the path is not set, cannot start FSW for Penumbra."); + return; + } + + Logger.LogDebug("Initializing Penumbra FSW on {path}", penumbraPath); + PenumbraWatcher = new() + { + Path = penumbraPath, + InternalBufferSize = 8388608, + NotifyFilter = NotifyFilters.CreationTime + | NotifyFilters.LastWrite + | NotifyFilters.FileName + | NotifyFilters.DirectoryName + | NotifyFilters.Size, + Filter = "*.*", + IncludeSubdirectories = true + }; + + PenumbraWatcher.Deleted += Fs_Changed; + PenumbraWatcher.Created += Fs_Changed; + PenumbraWatcher.Changed += Fs_Changed; + PenumbraWatcher.Renamed += Fs_Renamed; + PenumbraWatcher.EnableRaisingEvents = true; + } + + 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 (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created)) + return; + + lock (_watcherChanges) + { + _watcherChanges[e.FullPath] = new(e.ChangeType); + } + + Logger.LogTrace("FSW {event}: {path}", e.ChangeType, e.FullPath); + + _ = PenumbraWatcherExecution(); + } + + private void Fs_Renamed(object sender, RenamedEventArgs e) + { + if (Directory.Exists(e.FullPath)) + { + var directoryFiles = Directory.GetFiles(e.FullPath, "*.*", SearchOption.AllDirectories); + lock (_watcherChanges) + { + foreach (var file in directoryFiles) + { + if (!AllowedFileExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) continue; + var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase); + + _watcherChanges.Remove(oldPath); + _watcherChanges[file] = new(WatcherChangeTypes.Renamed, oldPath); + Logger.LogTrace("FSW Renamed: {path} -> {new}", oldPath, file); + + } + } + } + else + { + if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; + + lock (_watcherChanges) + { + _watcherChanges.Remove(e.OldFullPath); + _watcherChanges[e.FullPath] = new(WatcherChangeTypes.Renamed, e.OldFullPath); + } + + Logger.LogTrace("FSW Renamed: {path} -> {new}", e.OldFullPath, e.FullPath); + } + + _ = PenumbraWatcherExecution(); + } + + private CancellationTokenSource _penumbraFswCts = new(); + private CancellationTokenSource _mareFswCts = new(); + private CancellationTokenSource _substFswCts = new(); + public FileSystemWatcher? PenumbraWatcher { get; private set; } + public FileSystemWatcher? MareWatcher { get; private set; } + public FileSystemWatcher? SubstWatcher { get; private set; } + + private async Task MareWatcherExecution() + { + _mareFswCts = _mareFswCts.CancelRecreate(); + var token = _mareFswCts.Token; + var delay = TimeSpan.FromSeconds(5); + Dictionary changes; + lock (_mareChanges) + changes = _mareChanges.ToDictionary(t => t.Key, t => t.Value, StringComparer.Ordinal); + try + { + do + { + await Task.Delay(delay, token).ConfigureAwait(false); + } while (HaltScanLocks.Any(f => f.Value.Value > 0)); + } + catch (TaskCanceledException) + { + return; + } + + lock (_mareChanges) + { + foreach (var key in changes.Keys) + { + _mareChanges.Remove(key); + } + } + + HandleChanges(changes); + } + + private async Task SubstWatcherExecution() + { + _substFswCts = _substFswCts.CancelRecreate(); + var token = _substFswCts.Token; + var delay = TimeSpan.FromSeconds(5); + Dictionary changes; + lock (_substChanges) + changes = _substChanges.ToDictionary(t => t.Key, t => t.Value, StringComparer.Ordinal); + try + { + do + { + await Task.Delay(delay, token).ConfigureAwait(false); + } while (HaltScanLocks.Any(f => f.Value.Value > 0)); + } + catch (TaskCanceledException) + { + return; + } + + lock (_substChanges) + { + foreach (var key in changes.Keys) + { + _substChanges.Remove(key); + } + } + + HandleChanges(changes); + } + + public void ClearSubstStorage() + { + var substDir = _fileDbManager.SubstFolder; + var allSubstFiles = Directory.GetFiles(substDir, "*.*", SearchOption.TopDirectoryOnly) + .Where(f => + { + var val = f.Split('\\')[^1]; + return val.Length == 40 || (val.Split('.').FirstOrDefault()?.Length ?? 0) == 40 + || val.EndsWith(".tmp", StringComparison.OrdinalIgnoreCase); + }); + if (SubstWatcher != null) + SubstWatcher.EnableRaisingEvents = false; + + Dictionary changes = _substChanges.ToDictionary(t => t.Key, t => new WatcherChange(WatcherChangeTypes.Deleted, t.Key), StringComparer.Ordinal); + + foreach (var file in allSubstFiles) + { + try + { + File.Delete(file); + } + catch { } + } + + HandleChanges(changes); + + if (SubstWatcher != null) + SubstWatcher.EnableRaisingEvents = true; + } + + public void DeleteSubstOriginals() + { + var cacheDir = _configService.Current.CacheFolder; + var substDir = _fileDbManager.SubstFolder; + var allSubstFiles = Directory.GetFiles(substDir, "*.*", SearchOption.TopDirectoryOnly) + .Where(f => + { + var val = f.Split('\\')[^1]; + return val.Length == 40 || (val.Split('.').FirstOrDefault()?.Length ?? 0) == 40 + || val.EndsWith(".tmp", StringComparison.OrdinalIgnoreCase); + }); + + foreach (var substFile in allSubstFiles) + { + var cacheFile = Path.Join(cacheDir, Path.GetFileName(substFile)); + try + { + if (File.Exists(cacheFile)) + File.Delete(cacheFile); + } + catch { } + } + } + + private void HandleChanges(Dictionary changes) + { + lock (_fileDbManager) + { + var deletedEntries = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Deleted).Select(c => c.Key); + var renamedEntries = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Renamed); + var remainingEntries = changes.Where(c => c.Value.ChangeType != WatcherChangeTypes.Deleted).Select(c => c.Key); + + foreach (var entry in deletedEntries) + { + Logger.LogDebug("FSW Change: Deletion - {val}", entry); + } + + foreach (var entry in renamedEntries) + { + Logger.LogDebug("FSW Change: Renamed - {oldVal} => {val}", entry.Value.OldPath, entry.Key); + } + + foreach (var entry in remainingEntries) + { + Logger.LogDebug("FSW Change: Creation or Change - {val}", entry); + } + + var allChanges = deletedEntries + .Concat(renamedEntries.Select(c => c.Value.OldPath!)) + .Concat(renamedEntries.Select(c => c.Key)) + .Concat(remainingEntries) + .ToArray(); + + _ = _fileDbManager.GetFileCachesByPaths(allChanges); + + _fileDbManager.WriteOutFullCsv(); + } + } + + private async Task PenumbraWatcherExecution() + { + _penumbraFswCts = _penumbraFswCts.CancelRecreate(); + var token = _penumbraFswCts.Token; + Dictionary changes; + lock (_watcherChanges) + changes = _watcherChanges.ToDictionary(t => t.Key, t => t.Value, StringComparer.Ordinal); + var delay = TimeSpan.FromSeconds(10); + try + { + do + { + await Task.Delay(delay, token).ConfigureAwait(false); + } while (HaltScanLocks.Any(f => f.Value.Value > 0)); + } + catch (TaskCanceledException) + { + return; + } + + lock (_watcherChanges) + { + foreach (var key in changes.Keys) + { + _watcherChanges.Remove(key); + } + } + + HandleChanges(changes); + } + + public void InvokeScan() + { + TotalFiles = 0; + _currentFileProgress = 0; + _scanCancellationTokenSource = _scanCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource(); + var token = _scanCancellationTokenSource.Token; + _ = Task.Run(async () => + { + Logger.LogDebug("Starting Full File Scan"); + TotalFiles = 0; + _currentFileProgress = 0; + while (_dalamudUtil.IsOnFrameworkThread) + { + Logger.LogWarning("Scanner is on framework, waiting for leaving thread before continuing"); + await Task.Delay(250, token).ConfigureAwait(false); + } + + Thread scanThread = new(() => + { + try + { + _performanceCollector.LogPerformance(this, $"FullFileScan", () => FullFileScan(token)); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error during Full File Scan"); + } + }) + { + Priority = ThreadPriority.Lowest, + IsBackground = true + }; + scanThread.Start(); + while (scanThread.IsAlive) + { + await Task.Delay(250).ConfigureAwait(false); + } + TotalFiles = 0; + _currentFileProgress = 0; + }, token); + } + + public void RecalculateFileCacheSize(CancellationToken token) + { + if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder)) + { + FileCacheSize = 0; + return; + } + + FileCacheSize = -1; + DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName); + try + { + FileCacheDriveFree = di.AvailableFreeSpace; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Could not determine drive size for Storage Folder {folder}", _configService.Current.CacheFolder); + } + + var files = Directory.EnumerateFiles(_configService.Current.CacheFolder) + .Concat(Directory.EnumerateFiles(_fileDbManager.SubstFolder)) + .Select(f => new FileInfo(f)) + .OrderBy(f => f.LastAccessTime).ToList(); + FileCacheSize = files + .Sum(f => + { + token.ThrowIfCancellationRequested(); + + try + { + return _fileCompactor.GetFileSizeOnDisk(f, StorageisNTFS); + } + catch + { + return 0; + } + }); + + var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d); + + if (FileCacheSize < maxCacheInBytes) return; + + var substDir = _fileDbManager.SubstFolder; + + var maxCacheBuffer = maxCacheInBytes * 0.05d; + while (FileCacheSize > maxCacheInBytes - (long)maxCacheBuffer) + { + var oldestFile = files[0]; + FileCacheSize -= _fileCompactor.GetFileSizeOnDisk(oldestFile); + File.Delete(oldestFile.FullName); + files.Remove(oldestFile); + } + } + + public void ResetLocks() + { + HaltScanLocks.Clear(); + } + + public void ResumeScan(string source) + { + HaltScanLocks.TryAdd(source, new(0)); + Interlocked.Decrement(ref HaltScanLocks[source].Value); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _scanCancellationTokenSource?.Cancel(); + PenumbraWatcher?.Dispose(); + MareWatcher?.Dispose(); + SubstWatcher?.Dispose(); + _penumbraFswCts?.CancelDispose(); + _mareFswCts?.CancelDispose(); + _substFswCts?.CancelDispose(); + _periodicCalculationTokenSource?.CancelDispose(); + } + + private void FullFileScan(CancellationToken ct) + { + TotalFiles = 1; + var penumbraDir = _ipcManager.Penumbra.ModDirectory; + bool penDirExists = true; + bool cacheDirExists = true; + var substDir = _fileDbManager.SubstFolder; + if (string.IsNullOrEmpty(penumbraDir) || !Directory.Exists(penumbraDir)) + { + penDirExists = false; + Logger.LogWarning("Penumbra directory is not set or does not exist."); + } + if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder)) + { + cacheDirExists = false; + Logger.LogWarning("Snowcloak Cache directory is not set or does not exist."); + } + if (!penDirExists || !cacheDirExists) + { + return; + } + + try + { + if (!Directory.Exists(substDir)) + Directory.CreateDirectory(substDir); + } + catch + { + Logger.LogWarning("Could not create subst directory at {path}.", substDir); + } + + var previousThreadPriority = Thread.CurrentThread.Priority; + Thread.CurrentThread.Priority = ThreadPriority.Lowest; + Logger.LogDebug("Getting files from {penumbra} and {storage}", penumbraDir, _configService.Current.CacheFolder); + + Dictionary penumbraFiles = new(StringComparer.Ordinal); + foreach (var folder in Directory.EnumerateDirectories(penumbraDir!)) + { + try + { + penumbraFiles[folder] = + [ + .. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories) + .AsParallel() + .Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase)) + && !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase) + && !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase) + && !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)), + ]; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Could not enumerate path {path}", folder); + } + Thread.Sleep(50); + if (ct.IsCancellationRequested) return; + } + + var allCacheFiles = Directory.GetFiles(_configService.Current.CacheFolder, "*.*", SearchOption.TopDirectoryOnly) + .Concat(Directory.GetFiles(substDir, "*.*", SearchOption.TopDirectoryOnly)) + .AsParallel() + .Where(f => + { + var val = f.Split('\\')[^1]; + return val.Length == 40 || (val.Split('.').FirstOrDefault()?.Length ?? 0) == 40; + }); + + if (ct.IsCancellationRequested) return; + + var allScannedFiles = (penumbraFiles.SelectMany(k => k.Value)) + .Concat(allCacheFiles) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToDictionary(t => t.ToLowerInvariant(), t => false, StringComparer.OrdinalIgnoreCase); + + TotalFiles = allScannedFiles.Count; + Thread.CurrentThread.Priority = previousThreadPriority; + + Thread.Sleep(TimeSpan.FromSeconds(2)); + + if (ct.IsCancellationRequested) return; + + // scan files from database + var threadCount = Math.Clamp((int)(Environment.ProcessorCount / 2.0f), 2, 8); + + List entitiesToRemove = []; + List entitiesToUpdate = []; + Lock sync = new(); + Thread[] workerThreads = new Thread[threadCount]; + + ConcurrentQueue fileCaches = new(_fileDbManager.GetAllFileCaches()); + + TotalFilesStorage = fileCaches.Count; + + for (int i = 0; i < threadCount; i++) + { + Logger.LogTrace("Creating Thread {i}", i); + workerThreads[i] = new((tcounter) => + { + var threadNr = (int)tcounter!; + Logger.LogTrace("Spawning Worker Thread {i}", threadNr); + while (!ct.IsCancellationRequested && fileCaches.TryDequeue(out var workload)) + { + try + { + if (ct.IsCancellationRequested) return; + + if (!_ipcManager.Penumbra.APIAvailable) + { + Logger.LogWarning("Penumbra not available"); + return; + } + + var validatedCacheResult = _fileDbManager.ValidateFileCacheEntity(workload); + if (validatedCacheResult.State != FileState.RequireDeletion) + { + lock (sync) { allScannedFiles[validatedCacheResult.FileCache.ResolvedFilepath] = true; } + } + if (validatedCacheResult.State == FileState.RequireUpdate) + { + Logger.LogTrace("To update: {path}", validatedCacheResult.FileCache.ResolvedFilepath); + lock (sync) { entitiesToUpdate.Add(validatedCacheResult.FileCache); } + } + else if (validatedCacheResult.State == FileState.RequireDeletion) + { + Logger.LogTrace("To delete: {path}", validatedCacheResult.FileCache.ResolvedFilepath); + lock (sync) { entitiesToRemove.Add(validatedCacheResult.FileCache); } + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed validating {path}", workload.ResolvedFilepath); + } + Interlocked.Increment(ref _currentFileProgress); + } + + Logger.LogTrace("Ending Worker Thread {i}", threadNr); + }) + { + Priority = ThreadPriority.Lowest, + IsBackground = true + }; + workerThreads[i].Start(i); + } + + while (!ct.IsCancellationRequested && workerThreads.Any(u => u.IsAlive)) + { + Thread.Sleep(1000); + } + + if (ct.IsCancellationRequested) return; + + Logger.LogTrace("Threads exited"); + + if (!_ipcManager.Penumbra.APIAvailable) + { + Logger.LogWarning("Penumbra not available"); + return; + } + + if (entitiesToUpdate.Any() || entitiesToRemove.Any()) + { + foreach (var entity in entitiesToUpdate) + { + _fileDbManager.UpdateHashedFile(entity); + } + + foreach (var entity in entitiesToRemove) + { + _fileDbManager.RemoveHashedFile(entity.Hash, entity.PrefixedFilePath); + } + + _fileDbManager.WriteOutFullCsv(); + } + + Logger.LogTrace("Scanner validated existing db files"); + + if (!_ipcManager.Penumbra.APIAvailable) + { + Logger.LogWarning("Penumbra not available"); + return; + } + + if (ct.IsCancellationRequested) return; + + // scan new files + if (allScannedFiles.Any(c => !c.Value)) + { + Parallel.ForEach(allScannedFiles.Where(c => !c.Value).Select(c => c.Key), + new ParallelOptions() + { + MaxDegreeOfParallelism = threadCount, + CancellationToken = ct + }, (cachePath) => + { + if (ct.IsCancellationRequested) return; + + if (!_ipcManager.Penumbra.APIAvailable) + { + Logger.LogWarning("Penumbra not available"); + return; + } + + try + { + var entry = _fileDbManager.CreateFileEntry(cachePath); + if (entry == null) + { + if (cachePath.StartsWith(substDir, StringComparison.Ordinal)) + _ = _fileDbManager.CreateSubstEntry(cachePath); + else + _ = _fileDbManager.CreateCacheEntry(cachePath); + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed adding {file}", cachePath); + } + + Interlocked.Increment(ref _currentFileProgress); + }); + + Logger.LogTrace("Scanner added {notScanned} new files to db", allScannedFiles.Count(c => !c.Value)); + } + + Logger.LogDebug("Scan complete"); + TotalFiles = 0; + _currentFileProgress = 0; + entitiesToRemove.Clear(); + allScannedFiles.Clear(); + + if (!_configService.Current.InitialScanComplete) + { + _configService.Current.InitialScanComplete = true; + _configService.Save(); + StartMareWatcher(_configService.Current.CacheFolder); + StartSubstWatcher(_fileDbManager.SubstFolder); + StartPenumbraWatcher(penumbraDir); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/FileCache/FileCacheEntity.cs b/MareSynchronos/FileCache/FileCacheEntity.cs new file mode 100644 index 0000000..e81353a --- /dev/null +++ b/MareSynchronos/FileCache/FileCacheEntity.cs @@ -0,0 +1,30 @@ +#nullable disable + +namespace MareSynchronos.FileCache; + +public class FileCacheEntity +{ + public FileCacheEntity(string hash, string path, string lastModifiedDateTicks, long? size = null, long? compressedSize = null) + { + Size = size; + CompressedSize = compressedSize; + Hash = hash; + PrefixedFilePath = path; + LastModifiedDateTicks = lastModifiedDateTicks; + } + + public long? CompressedSize { get; set; } + public string CsvEntry => $"{Hash}{FileCacheManager.CsvSplit}{PrefixedFilePath}{FileCacheManager.CsvSplit}{LastModifiedDateTicks}|{Size ?? -1}|{CompressedSize ?? -1}"; + public string Hash { get; set; } + public bool IsCacheEntry => PrefixedFilePath.StartsWith(FileCacheManager.CachePrefix, StringComparison.OrdinalIgnoreCase); + public bool IsSubstEntry => PrefixedFilePath.StartsWith(FileCacheManager.SubstPrefix, StringComparison.OrdinalIgnoreCase); + public string LastModifiedDateTicks { get; set; } + public string PrefixedFilePath { get; init; } + public string ResolvedFilepath { get; private set; } = string.Empty; + public long? Size { get; set; } + + public void SetResolvedFilePath(string filePath) + { + ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal); + } +} \ No newline at end of file diff --git a/MareSynchronos/FileCache/FileCacheManager.cs b/MareSynchronos/FileCache/FileCacheManager.cs new file mode 100644 index 0000000..6dd4477 --- /dev/null +++ b/MareSynchronos/FileCache/FileCacheManager.cs @@ -0,0 +1,556 @@ +using Dalamud.Utility; +using K4os.Compression.LZ4.Streams; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Globalization; +using System.Text; + +namespace MareSynchronos.FileCache; + +public sealed class FileCacheManager : IHostedService +{ + public const string CachePrefix = "{cache}"; + public const string CsvSplit = "|"; + public const string PenumbraPrefix = "{penumbra}"; + public const string SubstPrefix = "{subst}"; + public const string SubstPath = "subst"; + public string CacheFolder => _configService.Current.CacheFolder; + public string SubstFolder => CacheFolder.IsNullOrEmpty() ? string.Empty : CacheFolder.ToLowerInvariant().TrimEnd('\\') + "\\" + SubstPath; + private readonly MareConfigService _configService; + private readonly MareMediator _mareMediator; + private readonly string _csvPath; + private readonly ConcurrentDictionary> _fileCaches = new(StringComparer.Ordinal); + private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1); + private readonly Lock _fileWriteLock = new(); + private readonly IpcManager _ipcManager; + private readonly ILogger _logger; + + public FileCacheManager(ILogger logger, IpcManager ipcManager, MareConfigService configService, MareMediator mareMediator) + { + _logger = logger; + _ipcManager = ipcManager; + _configService = configService; + _mareMediator = mareMediator; + _csvPath = Path.Combine(configService.ConfigurationDirectory, "FileCache.csv"); + } + + private string CsvBakPath => _csvPath + ".bak"; + + public FileCacheEntity? CreateCacheEntry(string path, string? hash = null) + { + FileInfo fi = new(path); + if (!fi.Exists) return null; + _logger.LogTrace("Creating cache entry for {path}", path); + var fullName = fi.FullName.ToLowerInvariant(); + if (!fullName.Contains(_configService.Current.CacheFolder.ToLowerInvariant(), StringComparison.Ordinal)) return null; + string prefixedPath = fullName.Replace(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); + if (hash != null) + return CreateFileCacheEntity(fi, prefixedPath, hash); + else + return CreateFileCacheEntity(fi, prefixedPath); + } + + public FileCacheEntity? CreateSubstEntry(string path) + { + FileInfo fi = new(path); + if (!fi.Exists) return null; + _logger.LogTrace("Creating substitute entry for {path}", path); + var fullName = fi.FullName.ToLowerInvariant(); + if (!fullName.Contains(SubstFolder, StringComparison.Ordinal)) return null; + string prefixedPath = fullName.Replace(SubstFolder, SubstPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); + var fakeHash = Path.GetFileNameWithoutExtension(fi.FullName).ToUpperInvariant(); + var result = CreateFileCacheEntity(fi, prefixedPath, fakeHash); + return result; + } + + public FileCacheEntity? CreateFileEntry(string path) + { + FileInfo fi = new(path); + if (!fi.Exists) return null; + _logger.LogTrace("Creating file entry for {path}", path); + var fullName = fi.FullName.ToLowerInvariant(); + if (!fullName.Contains(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null; + string prefixedPath = fullName.Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); + return CreateFileCacheEntity(fi, prefixedPath); + } + + public List GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v).ToList(); + + public List GetAllFileCachesByHash(string hash, bool ignoreCacheEntries = false, bool validate = true) + { + List output = []; + if (_fileCaches.TryGetValue(hash, out var fileCacheEntities)) + { + foreach (var fileCache in fileCacheEntities.Where(c => ignoreCacheEntries ? (!c.IsCacheEntry && !c.IsSubstEntry) : true).ToList()) + { + if (!validate) output.Add(fileCache); + else + { + var validated = GetValidatedFileCache(fileCache); + if (validated != null) output.Add(validated); + } + } + } + + return output; + } + + public Task> ValidateLocalIntegrity(IProgress<(int, int, FileCacheEntity)> progress, CancellationToken cancellationToken) + { + _mareMediator.Publish(new HaltScanMessage(nameof(ValidateLocalIntegrity))); + _logger.LogInformation("Validating local storage"); + var cacheEntries = _fileCaches.SelectMany(v => v.Value).Where(v => v.IsCacheEntry).ToList(); + List brokenEntities = []; + int i = 0; + foreach (var fileCache in cacheEntries) + { + if (cancellationToken.IsCancellationRequested) break; + if (fileCache.IsSubstEntry) continue; + + _logger.LogInformation("Validating {file}", fileCache.ResolvedFilepath); + + progress.Report((i, cacheEntries.Count, fileCache)); + i++; + if (!File.Exists(fileCache.ResolvedFilepath)) + { + brokenEntities.Add(fileCache); + continue; + } + + try + { + var computedHash = Crypto.GetFileHash(fileCache.ResolvedFilepath); + if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal)) + { + _logger.LogInformation("Failed to validate {file}, got hash {hash}, expected hash {expectedHash}", fileCache.ResolvedFilepath, computedHash, fileCache.Hash); + brokenEntities.Add(fileCache); + } + } + catch (Exception e) + { + _logger.LogWarning(e, "Error during validation of {file}", fileCache.ResolvedFilepath); + brokenEntities.Add(fileCache); + } + } + + foreach (var brokenEntity in brokenEntities) + { + RemoveHashedFile(brokenEntity.Hash, brokenEntity.PrefixedFilePath); + + try + { + File.Delete(brokenEntity.ResolvedFilepath); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not delete {file}", brokenEntity.ResolvedFilepath); + } + } + + _mareMediator.Publish(new ResumeScanMessage(nameof(ValidateLocalIntegrity))); + return Task.FromResult(brokenEntities); + } + + public string GetCacheFilePath(string hash, string extension) + { + return Path.Combine(_configService.Current.CacheFolder, hash + "." + extension); + } + + public string GetSubstFilePath(string hash, string extension) + { + return Path.Combine(SubstFolder, hash + "." + extension); + } + + public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) + { + var fileCache = GetFileCacheByHash(fileHash)!; + using var fs = File.OpenRead(fileCache.ResolvedFilepath); + var ms = new MemoryStream(64 * 1024); + using var encstream = LZ4Stream.Encode(ms, new LZ4EncoderSettings(){CompressionLevel=K4os.Compression.LZ4.LZ4Level.L09_HC}); + await fs.CopyToAsync(encstream, uploadToken).ConfigureAwait(false); + encstream.Close(); + fileCache.CompressedSize = encstream.Length; + return (fileHash, ms.ToArray()); + } + + public FileCacheEntity? GetFileCacheByHash(string hash, bool preferSubst = false) + { + var caches = GetFileCachesByHash(hash); + if (preferSubst && caches.Subst != null) + return caches.Subst; + return caches.Penumbra ?? caches.Cache; + } + + public (FileCacheEntity? Penumbra, FileCacheEntity? Cache, FileCacheEntity? Subst) GetFileCachesByHash(string hash) + { + (FileCacheEntity? Penumbra, FileCacheEntity? Cache, FileCacheEntity? Subst) result = (null, null, null); + if (_fileCaches.TryGetValue(hash, out var hashes)) + { + result.Penumbra = hashes.Where(p => p.PrefixedFilePath.StartsWith(PenumbraPrefix, StringComparison.Ordinal)).Select(GetValidatedFileCache).FirstOrDefault(); + result.Cache = hashes.Where(p => p.PrefixedFilePath.StartsWith(CachePrefix, StringComparison.Ordinal)).Select(GetValidatedFileCache).FirstOrDefault(); + result.Subst = hashes.Where(p => p.PrefixedFilePath.StartsWith(SubstPrefix, StringComparison.Ordinal)).Select(GetValidatedFileCache).FirstOrDefault(); + } + return result; + } + + private FileCacheEntity? GetFileCacheByPath(string path) + { + var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant() + .Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase); + var entry = _fileCaches.SelectMany(v => v.Value).FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase)); + + if (entry == null) + { + _logger.LogDebug("Found no entries for {path}", cleanedPath); + return CreateFileEntry(path); + } + + var validatedCacheEntry = GetValidatedFileCache(entry); + + return validatedCacheEntry; + } + + public Dictionary GetFileCachesByPaths(string[] paths) + { + _getCachesByPathsSemaphore.Wait(); + + try + { + var cleanedPaths = paths.Distinct(StringComparer.OrdinalIgnoreCase).ToDictionary(p => p, + p => p.Replace("/", "\\", StringComparison.OrdinalIgnoreCase) + .Replace(_ipcManager.Penumbra.ModDirectory!, _ipcManager.Penumbra.ModDirectory!.EndsWith('\\') ? PenumbraPrefix + '\\' : PenumbraPrefix, StringComparison.OrdinalIgnoreCase) + .Replace(SubstFolder, SubstPrefix, StringComparison.OrdinalIgnoreCase) + .Replace(_configService.Current.CacheFolder, _configService.Current.CacheFolder.EndsWith('\\') ? CachePrefix + '\\' : CachePrefix, StringComparison.OrdinalIgnoreCase) + .Replace("\\\\", "\\", StringComparison.Ordinal), + StringComparer.OrdinalIgnoreCase); + + Dictionary result = new(StringComparer.OrdinalIgnoreCase); + + var dict = _fileCaches.SelectMany(f => f.Value) + .ToDictionary(d => d.PrefixedFilePath, d => d, StringComparer.OrdinalIgnoreCase); + + foreach (var entry in cleanedPaths) + { + //_logger.LogDebug("Checking {path}", entry.Value); + + if (dict.TryGetValue(entry.Value, out var entity)) + { + var validatedCache = GetValidatedFileCache(entity); + result.Add(entry.Key, validatedCache); + } + else + { + if (entry.Value.StartsWith(PenumbraPrefix, StringComparison.Ordinal)) + result.Add(entry.Key, CreateFileEntry(entry.Key)); + else if (entry.Value.StartsWith(SubstPrefix, StringComparison.Ordinal)) + result.Add(entry.Key, CreateSubstEntry(entry.Key)); + else if (entry.Value.StartsWith(CachePrefix, StringComparison.Ordinal)) + result.Add(entry.Key, CreateCacheEntry(entry.Key)); + } + } + + return result; + } + finally + { + _getCachesByPathsSemaphore.Release(); + } + } + + public void RemoveHashedFile(string hash, string prefixedFilePath) + { + if (_fileCaches.TryGetValue(hash, out var caches)) + { + var removedCount = caches?.RemoveAll(c => string.Equals(c.PrefixedFilePath, prefixedFilePath, StringComparison.Ordinal)); + _logger.LogTrace("Removed from DB: {count} file(s) with hash {hash} and file cache {path}", removedCount, hash, prefixedFilePath); + + if (caches?.Count == 0) + { + _fileCaches.Remove(hash, out var _); + } + } + } + + public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true) + { + _logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath); + var oldHash = fileCache.Hash; + var prefixedPath = fileCache.PrefixedFilePath; + if (computeProperties) + { + var fi = new FileInfo(fileCache.ResolvedFilepath); + fileCache.Size = fi.Length; + fileCache.CompressedSize = null; + fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath); + fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); + } + RemoveHashedFile(oldHash, prefixedPath); + AddHashedFile(fileCache); + } + + public (FileState State, FileCacheEntity FileCache) ValidateFileCacheEntity(FileCacheEntity fileCache) + { + fileCache = ReplacePathPrefixes(fileCache); + FileInfo fi = new(fileCache.ResolvedFilepath); + if (!fi.Exists) + { + return (FileState.RequireDeletion, fileCache); + } + if (!string.Equals(fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileCache.LastModifiedDateTicks, StringComparison.Ordinal)) + { + return (FileState.RequireUpdate, fileCache); + } + + return (FileState.Valid, fileCache); + } + + public void WriteOutFullCsv() + { + lock (_fileWriteLock) + { + StringBuilder sb = new(); + foreach (var entry in _fileCaches.SelectMany(k => k.Value).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase)) + { + sb.AppendLine(entry.CsvEntry); + } + + if (File.Exists(_csvPath)) + { + File.Copy(_csvPath, CsvBakPath, overwrite: true); + } + + try + { + File.WriteAllText(_csvPath, sb.ToString()); + File.Delete(CsvBakPath); + } + catch + { + File.WriteAllText(CsvBakPath, sb.ToString()); + } + } + } + + internal FileCacheEntity MigrateFileHashToExtension(FileCacheEntity fileCache, string ext) + { + try + { + RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath); + var extensionPath = fileCache.ResolvedFilepath.ToUpper(CultureInfo.InvariantCulture) + "." + ext; + File.Move(fileCache.ResolvedFilepath, extensionPath, overwrite: true); + var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture)); + newHashedEntity.SetResolvedFilePath(extensionPath); + AddHashedFile(newHashedEntity); + _logger.LogTrace("Migrated from {oldPath} to {newPath}", fileCache.ResolvedFilepath, newHashedEntity.ResolvedFilepath); + return newHashedEntity; + } + catch (Exception ex) + { + AddHashedFile(fileCache); + _logger.LogWarning(ex, "Failed to migrate entity {entity}", fileCache.PrefixedFilePath); + return fileCache; + } + } + + private void AddHashedFile(FileCacheEntity fileCache) + { + if (!_fileCaches.TryGetValue(fileCache.Hash, out var entries) || entries is null) + { + _fileCaches[fileCache.Hash] = entries = []; + } + + if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase))) + { + //_logger.LogTrace("Adding to DB: {hash} => {path}", fileCache.Hash, fileCache.PrefixedFilePath); + entries.Add(fileCache); + } + } + + private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null) + { + hash ??= Crypto.GetFileHash(fileInfo.FullName); + var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileInfo.Length); + entity = ReplacePathPrefixes(entity); + AddHashedFile(entity); + lock (_fileWriteLock) + { + File.AppendAllLines(_csvPath, new[] { entity.CsvEntry }); + } + var result = GetFileCacheByPath(fileInfo.FullName); + _logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null)); + return result; + } + + private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache) + { + var resultingFileCache = ReplacePathPrefixes(fileCache); + //_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath); + resultingFileCache = Validate(resultingFileCache); + return resultingFileCache; + } + + private FileCacheEntity ReplacePathPrefixes(FileCacheEntity fileCache) + { + if (fileCache.PrefixedFilePath.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase)) + { + fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(PenumbraPrefix, _ipcManager.Penumbra.ModDirectory, StringComparison.Ordinal)); + } + else if (fileCache.PrefixedFilePath.StartsWith(SubstPrefix, StringComparison.OrdinalIgnoreCase)) + { + fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(SubstPrefix, SubstFolder, StringComparison.Ordinal)); + } + else if (fileCache.PrefixedFilePath.StartsWith(CachePrefix, StringComparison.OrdinalIgnoreCase)) + { + fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(CachePrefix, _configService.Current.CacheFolder, StringComparison.Ordinal)); + } + + return fileCache; + } + + private FileCacheEntity? Validate(FileCacheEntity fileCache) + { + 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; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting FileCacheManager"); + lock (_fileWriteLock) + { + try + { + _logger.LogInformation("Checking for {bakPath}", CsvBakPath); + + if (File.Exists(CsvBakPath)) + { + _logger.LogInformation("{bakPath} found, moving to {csvPath}", CsvBakPath, _csvPath); + + File.Move(CsvBakPath, _csvPath, overwrite: true); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to move BAK to ORG, deleting BAK"); + try + { + if (File.Exists(CsvBakPath)) + File.Delete(CsvBakPath); + } + catch (Exception ex1) + { + _logger.LogWarning(ex1, "Could not delete bak file"); + } + } + } + + if (File.Exists(_csvPath)) + { + if (!_ipcManager.Penumbra.APIAvailable || string.IsNullOrEmpty(_ipcManager.Penumbra.ModDirectory)) + { + _mareMediator.Publish(new NotificationMessage("Penumbra not connected", + "Could not load local file cache data. Penumbra is not connected or not properly set up. Please enable and/or configure Penumbra properly to use Snowcloak. After, reload Snowcloak in the Plugin installer.", + MareConfiguration.Models.NotificationType.Error)); + } + + _logger.LogInformation("{csvPath} found, parsing", _csvPath); + + bool success = false; + string[] entries = []; + int attempts = 0; + while (!success && attempts < 10) + { + try + { + _logger.LogInformation("Attempting to read {csvPath}", _csvPath); + entries = File.ReadAllLines(_csvPath); + success = true; + } + catch (Exception ex) + { + attempts++; + _logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath); + Thread.Sleep(100); + } + } + + if (!entries.Any()) + { + _logger.LogWarning("Could not load entries from {path}, continuing with empty file cache", _csvPath); + } + + _logger.LogInformation("Found {amount} files in {path}", entries.Length, _csvPath); + + Dictionary processedFiles = new(StringComparer.OrdinalIgnoreCase); + foreach (var entry in entries) + { + var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None); + try + { + var hash = splittedEntry[0]; + if (hash.Length != 40) throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length); + var path = splittedEntry[1]; + var time = splittedEntry[2]; + + if (processedFiles.ContainsKey(path)) + { + _logger.LogWarning("Already processed {file}, ignoring", path); + continue; + } + + processedFiles.Add(path, value: true); + + long size = -1; + long compressed = -1; + if (splittedEntry.Length > 3) + { + if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result)) + { + size = result; + } + if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed)) + { + compressed = resultCompressed; + } + } + AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed))); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry); + } + } + + if (processedFiles.Count != entries.Length) + { + WriteOutFullCsv(); + } + } + + _logger.LogInformation("Started FileCacheManager"); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + WriteOutFullCsv(); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/MareSynchronos/FileCache/FileCompactor.cs b/MareSynchronos/FileCache/FileCompactor.cs new file mode 100644 index 0000000..b48c516 --- /dev/null +++ b/MareSynchronos/FileCache/FileCompactor.cs @@ -0,0 +1,250 @@ +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services; +using Microsoft.Extensions.Logging; +using System.Runtime.InteropServices; + +namespace MareSynchronos.FileCache; + +public sealed class FileCompactor +{ + public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U; + public const ulong WOF_PROVIDER_FILE = 2UL; + + private readonly Dictionary _clusterSizes; + + private readonly WOF_FILE_COMPRESSION_INFO_V1 _efInfo; + private readonly ILogger _logger; + + private readonly MareConfigService _mareConfigService; + private readonly DalamudUtilService _dalamudUtilService; + + public FileCompactor(ILogger logger, MareConfigService mareConfigService, DalamudUtilService dalamudUtilService) + { + _clusterSizes = new(StringComparer.Ordinal); + _logger = logger; + _mareConfigService = mareConfigService; + _dalamudUtilService = dalamudUtilService; + _efInfo = new WOF_FILE_COMPRESSION_INFO_V1 + { + Algorithm = CompressionAlgorithm.XPRESS8K, + Flags = 0 + }; + } + + private enum CompressionAlgorithm + { + NO_COMPRESSION = -2, + LZNT1 = -1, + XPRESS4K = 0, + LZX = 1, + XPRESS8K = 2, + XPRESS16K = 3 + } + + public bool MassCompactRunning { get; private set; } = false; + + public string Progress { get; private set; } = string.Empty; + + public void CompactStorage(bool compress) + { + MassCompactRunning = true; + + int currentFile = 1; + var allFiles = Directory.EnumerateFiles(_mareConfigService.Current.CacheFolder).ToList(); + int allFilesCount = allFiles.Count; + foreach (var file in allFiles) + { + Progress = $"{currentFile}/{allFilesCount}"; + if (compress) + CompactFile(file); + else + DecompressFile(file); + currentFile++; + } + + MassCompactRunning = false; + } + + public long GetFileSizeOnDisk(FileInfo fileInfo, bool? isNTFS = null) + { + bool ntfs = isNTFS ?? string.Equals(new DriveInfo(fileInfo.Directory!.Root.FullName).DriveFormat, "NTFS", StringComparison.OrdinalIgnoreCase); + + if (_dalamudUtilService.IsWine || !ntfs) return fileInfo.Length; + + var clusterSize = GetClusterSize(fileInfo); + if (clusterSize == -1) return fileInfo.Length; + var losize = GetCompressedFileSizeW(fileInfo.FullName, out uint hosize); + var size = (long)hosize << 32 | losize; + return ((size + clusterSize - 1) / clusterSize) * clusterSize; + } + + public async Task WriteAllBytesAsync(string filePath, byte[] decompressedFile, CancellationToken token) + { + await File.WriteAllBytesAsync(filePath, decompressedFile, token).ConfigureAwait(false); + + if (_dalamudUtilService.IsWine || !_mareConfigService.Current.UseCompactor) + { + return; + } + + CompactFile(filePath); + } + + public void RenameAndCompact(string filePath, string originalFilePath) + { + try + { + File.Move(originalFilePath, filePath); + } + catch (IOException) + { + // File already exists + return; + } + + if (_dalamudUtilService.IsWine || !_mareConfigService.Current.UseCompactor) + { + return; + } + + CompactFile(filePath); + } + + [DllImport("kernel32.dll")] + private static extern int DeviceIoControl(IntPtr hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, out IntPtr lpBytesReturned, out IntPtr lpOverlapped); + + [DllImport("kernel32.dll")] + private static extern uint GetCompressedFileSizeW([In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName, + [Out, MarshalAs(UnmanagedType.U4)] out uint lpFileSizeHigh); + + [DllImport("kernel32.dll", SetLastError = true, PreserveSig = true)] + private static extern int GetDiskFreeSpaceW([In, MarshalAs(UnmanagedType.LPWStr)] string lpRootPathName, + out uint lpSectorsPerCluster, out uint lpBytesPerSector, out uint lpNumberOfFreeClusters, + out uint lpTotalNumberOfClusters); + + [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); + + [DllImport("WofUtil.dll")] + private static extern int WofSetFileDataLocation(IntPtr FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length); + + private void CompactFile(string filePath) + { + var fs = new DriveInfo(new FileInfo(filePath).Directory!.Root.FullName); + bool isNTFS = string.Equals(fs.DriveFormat, "NTFS", StringComparison.OrdinalIgnoreCase); + if (!isNTFS) + { + _logger.LogWarning("Drive for file {file} is not NTFS", filePath); + return; + } + + var fi = new FileInfo(filePath); + var oldSize = fi.Length; + var clusterSize = GetClusterSize(fi); + + if (oldSize < Math.Max(clusterSize, 8 * 1024)) + { + _logger.LogDebug("File {file} is smaller than cluster size ({size}), ignoring", filePath, clusterSize); + return; + } + + if (!IsCompactedFile(filePath)) + { + _logger.LogDebug("Compacting file to XPRESS8K: {file}", filePath); + + WOFCompressFile(filePath); + + var newSize = GetFileSizeOnDisk(fi); + + _logger.LogDebug("Compressed {file} from {orig}b to {comp}b", filePath, oldSize, newSize); + } + else + { + _logger.LogDebug("File {file} already compressed", filePath); + } + } + + private void DecompressFile(string path) + { + _logger.LogDebug("Removing compression from {file}", path); + try + { + using (var fs = new FileStream(path, FileMode.Open)) + { +#pragma warning disable S3869 // "SafeHandle.DangerousGetHandle" should not be called + var hDevice = fs.SafeFileHandle.DangerousGetHandle(); +#pragma warning restore S3869 // "SafeHandle.DangerousGetHandle" should not be called + _ = DeviceIoControl(hDevice, FSCTL_DELETE_EXTERNAL_BACKING, nint.Zero, 0, nint.Zero, 0, out _, out _); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error decompressing file {path}", path); + } + } + + private int GetClusterSize(FileInfo fi) + { + if (!fi.Exists) return -1; + var root = fi.Directory?.Root.FullName.ToLower() ?? string.Empty; + if (string.IsNullOrEmpty(root)) return -1; + if (_clusterSizes.TryGetValue(root, out int value)) return value; + _logger.LogDebug("Getting Cluster Size for {path}, root {root}", fi.FullName, root); + int result = GetDiskFreeSpaceW(root, out uint sectorsPerCluster, out uint bytesPerSector, out _, out _); + if (result == 0) return -1; + _clusterSizes[root] = (int)(sectorsPerCluster * bytesPerSector); + _logger.LogDebug("Determined Cluster Size for root {root}: {cluster}", root, _clusterSizes[root]); + return _clusterSizes[root]; + } + + private static bool IsCompactedFile(string filePath) + { + uint buf = 8; + _ = WofIsExternalFile(filePath, out int isExtFile, out uint _, out var info, ref buf); + if (isExtFile == 0) return false; + return info.Algorithm == CompressionAlgorithm.XPRESS8K; + } + + private void WOFCompressFile(string path) + { + var efInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(_efInfo)); + Marshal.StructureToPtr(_efInfo, efInfoPtr, fDeleteOld: true); + ulong length = (ulong)Marshal.SizeOf(_efInfo); + try + { + using (var fs = new FileStream(path, FileMode.Open)) + { +#pragma warning disable S3869 // "SafeHandle.DangerousGetHandle" should not be called + var hFile = fs.SafeFileHandle.DangerousGetHandle(); +#pragma warning restore S3869 // "SafeHandle.DangerousGetHandle" should not be called + if (fs.SafeFileHandle.IsInvalid) + { + _logger.LogWarning("Invalid file handle to {file}", path); + } + else + { + var ret = WofSetFileDataLocation(hFile, WOF_PROVIDER_FILE, efInfoPtr, length); + if (!(ret == 0 || ret == unchecked((int)0x80070158))) + { + _logger.LogWarning("Failed to compact {file}: {ret}", path, ret.ToString("X")); + } + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error compacting file {path}", path); + } + finally + { + Marshal.FreeHGlobal(efInfoPtr); + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct WOF_FILE_COMPRESSION_INFO_V1 + { + public CompressionAlgorithm Algorithm; + public ulong Flags; + } +} \ No newline at end of file diff --git a/MareSynchronos/FileCache/FileState.cs b/MareSynchronos/FileCache/FileState.cs new file mode 100644 index 0000000..7f10e4e --- /dev/null +++ b/MareSynchronos/FileCache/FileState.cs @@ -0,0 +1,8 @@ +namespace MareSynchronos.FileCache; + +public enum FileState +{ + Valid, + RequireUpdate, + RequireDeletion, +} \ No newline at end of file diff --git a/MareSynchronos/FileCache/TransientResourceManager.cs b/MareSynchronos/FileCache/TransientResourceManager.cs new file mode 100644 index 0000000..e59e26d --- /dev/null +++ b/MareSynchronos/FileCache/TransientResourceManager.cs @@ -0,0 +1,313 @@ +using MareSynchronos.API.Data.Enum; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Data; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace MareSynchronos.FileCache; + +public sealed class TransientResourceManager : DisposableMediatorSubscriberBase +{ + private readonly Lock _cacheAdditionLock = new(); + private readonly HashSet _cachedHandledPaths = new(StringComparer.Ordinal); + private readonly TransientConfigService _configurationService; + private readonly DalamudUtilService _dalamudUtil; + private readonly string[] _fileTypesToHandle = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk"]; + private readonly HashSet _playerRelatedPointers = []; + private ConcurrentDictionary _cachedFrameAddresses = []; + + public TransientResourceManager(ILogger logger, TransientConfigService configurationService, + DalamudUtilService dalamudUtil, MareMediator mediator) : base(logger, mediator) + { + _configurationService = configurationService; + _dalamudUtil = dalamudUtil; + + Mediator.Subscribe(this, Manager_PenumbraResourceLoadEvent); + Mediator.Subscribe(this, (_) => Manager_PenumbraModSettingChanged()); + Mediator.Subscribe(this, (_) => DalamudUtil_FrameworkUpdate()); + Mediator.Subscribe(this, (msg) => + { + if (_playerRelatedPointers.Contains(msg.GameObjectHandler)) + { + DalamudUtil_ClassJobChanged(); + } + }); + Mediator.Subscribe(this, (msg) => + { + if (!msg.OwnedObject) return; + _playerRelatedPointers.Add(msg.GameObjectHandler); + }); + Mediator.Subscribe(this, (msg) => + { + if (!msg.OwnedObject) return; + _playerRelatedPointers.Remove(msg.GameObjectHandler); + }); + } + + private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult() + "_" + _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(); + private ConcurrentDictionary>? _semiTransientResources = null; + private ConcurrentDictionary> SemiTransientResources + { + get + { + if (_semiTransientResources == null) + { + _semiTransientResources = new(); + _semiTransientResources.TryAdd(ObjectKind.Player, new HashSet(StringComparer.Ordinal)); + if (_configurationService.Current.PlayerPersistentTransientCache.TryGetValue(PlayerPersistentDataKey, out var gamePaths)) + { + int restored = 0; + foreach (var gamePath in gamePaths) + { + if (string.IsNullOrEmpty(gamePath)) continue; + + try + { + Logger.LogDebug("Loaded persistent transient resource {path}", gamePath); + SemiTransientResources[ObjectKind.Player].Add(gamePath); + restored++; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error during loading persistent transient resource {path}", gamePath); + } + } + Logger.LogDebug("Restored {restored}/{total} semi persistent resources", restored, gamePaths.Count); + } + } + + return _semiTransientResources; + } + } + private ConcurrentDictionary> TransientResources { get; } = new(); + + public void CleanUpSemiTransientResources(ObjectKind objectKind, List? fileReplacement = null) + { + if (SemiTransientResources.TryGetValue(objectKind, out HashSet? value)) + { + if (fileReplacement == null) + { + value.Clear(); + return; + } + + foreach (var replacement in fileReplacement.Where(p => !p.HasFileReplacement).SelectMany(p => p.GamePaths).ToList()) + { + value.RemoveWhere(p => string.Equals(p, replacement, StringComparison.OrdinalIgnoreCase)); + } + } + } + + public HashSet GetSemiTransientResources(ObjectKind objectKind) + { + if (SemiTransientResources.TryGetValue(objectKind, out var result)) + { + return result ?? new HashSet(StringComparer.Ordinal); + } + + return new HashSet(StringComparer.Ordinal); + } + + public List GetTransientResources(IntPtr gameObject) + { + if (TransientResources.TryGetValue(gameObject, out var result)) + { + return [.. result]; + } + + return []; + } + + public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind) + { + if (!SemiTransientResources.TryGetValue(objectKind, out HashSet? value)) + { + value = new HashSet(StringComparer.Ordinal); + SemiTransientResources[objectKind] = value; + } + + if (!TransientResources.TryGetValue(gameObject, out var resources)) + { + return; + } + + var transientResources = resources.ToList(); + Logger.LogDebug("Persisting {count} transient resources", transientResources.Count); + foreach (var gamePath in transientResources) + { + value.Add(gamePath); + } + + if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(ObjectKind.Player, out var fileReplacements)) + { + _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = fileReplacements.Where(f => !string.IsNullOrEmpty(f)).ToHashSet(StringComparer.Ordinal); + _configurationService.Save(); + } + TransientResources[gameObject].Clear(); + } + + internal void AddSemiTransientResource(ObjectKind objectKind, string item) + { + if (!SemiTransientResources.TryGetValue(objectKind, out HashSet? value)) + { + value = new HashSet(StringComparer.Ordinal); + SemiTransientResources[objectKind] = value; + } + + value.Add(item.ToLowerInvariant()); + } + + internal void ClearTransientPaths(IntPtr ptr, List list) + { + if (TransientResources.TryGetValue(ptr, out var set)) + { + foreach (var file in set.Where(p => list.Contains(p, StringComparer.OrdinalIgnoreCase))) + { + Logger.LogTrace("Removing From Transient: {file}", file); + } + + int removed = set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase)); + Logger.LogInformation("Removed {removed} previously existing transient paths", removed); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + try + { + TransientResources.Clear(); + SemiTransientResources.Clear(); + if (SemiTransientResources.TryGetValue(ObjectKind.Player, out HashSet? value)) + { + _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = value; + _configurationService.Save(); + } + } + catch { } + } + + private void DalamudUtil_ClassJobChanged() + { + if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet? value)) + { + value?.Clear(); + } + } + + private void DalamudUtil_FrameworkUpdate() + { + _cachedFrameAddresses = _cachedFrameAddresses = new ConcurrentDictionary(_playerRelatedPointers.Where(k => k.Address != nint.Zero).ToDictionary(c => c.CurrentAddress(), c => c.ObjectKind)); + lock (_cacheAdditionLock) + { + _cachedHandledPaths.Clear(); + } + foreach (var item in TransientResources.Where(item => !_dalamudUtil.IsGameObjectPresent(item.Key)).Select(i => i.Key).ToList()) + { + Logger.LogDebug("Object not present anymore: {addr}", item.ToString("X")); + TransientResources.TryRemove(item, out _); + } + } + + private void Manager_PenumbraModSettingChanged() + { + _ = Task.Run(() => + { + Logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources"); + foreach (var item in _playerRelatedPointers) + { + Mediator.Publish(new TransientResourceChangedMessage(item.Address)); + } + }); + } + + private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg) + { + var gamePath = msg.GamePath.ToLowerInvariant(); + var gameObject = msg.GameObject; + var filePath = msg.FilePath; + + // ignore files already processed this frame + if (_cachedHandledPaths.Contains(gamePath)) return; + + lock (_cacheAdditionLock) + { + _cachedHandledPaths.Add(gamePath); + } + + // replace individual mtrl stuff + if (filePath.StartsWith("|", StringComparison.OrdinalIgnoreCase)) + { + filePath = filePath.Split("|")[2]; + } + // replace filepath + filePath = filePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase); + + // ignore files that are the same + var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase); + if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase)) return; + + // ignore files to not handle + if (!_fileTypesToHandle.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase))) + { + lock (_cacheAdditionLock) + { + _cachedHandledPaths.Add(gamePath); + } + return; + } + + // ignore files not belonging to anything player related + if (!_cachedFrameAddresses.TryGetValue(gameObject, out var objectKind)) + { + lock (_cacheAdditionLock) + { + _cachedHandledPaths.Add(gamePath); + } + return; + } + + if (!TransientResources.TryGetValue(gameObject, out HashSet? value)) + { + value = new(StringComparer.OrdinalIgnoreCase); + TransientResources[gameObject] = value; + } + + if (value.Contains(replacedGamePath) || + SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase))) + { + Logger.LogTrace("Not adding {replacedPath} : {filePath}", replacedGamePath, filePath); + } + else + { + var thing = _playerRelatedPointers.FirstOrDefault(f => f.Address == gameObject); + value.Add(replacedGamePath); + Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, thing?.ToString() ?? gameObject.ToString("X"), filePath); + _ = Task.Run(async () => + { + _sendTransientCts?.Cancel(); + _sendTransientCts?.Dispose(); + _sendTransientCts = new(); + var token = _sendTransientCts.Token; + await Task.Delay(TimeSpan.FromSeconds(2), token).ConfigureAwait(false); + Mediator.Publish(new TransientResourceChangedMessage(gameObject)); + }); + } + } + + internal void RemoveTransientResource(ObjectKind objectKind, string path) + { + if (SemiTransientResources.TryGetValue(objectKind, out var resources)) + { + resources.RemoveWhere(f => string.Equals(path, f, StringComparison.OrdinalIgnoreCase)); + _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = resources; + _configurationService.Save(); + } + } + + private CancellationTokenSource _sendTransientCts = new(); +} \ No newline at end of file diff --git a/MareSynchronos/GlobalSuppressions.cs b/MareSynchronos/GlobalSuppressions.cs new file mode 100644 index 0000000..ac112b6 --- /dev/null +++ b/MareSynchronos/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "", Scope = "member", Target = "~M:MareSynchronos.Services.CharaDataManager.AttachPoseData(MareSynchronos.API.Dto.CharaData.PoseEntry,MareSynchronos.Services.CharaData.Models.CharaDataExtendedUpdateDto)")] diff --git a/MareSynchronos/Interop/BlockedCharacterHandler.cs b/MareSynchronos/Interop/BlockedCharacterHandler.cs new file mode 100644 index 0000000..f8d348f --- /dev/null +++ b/MareSynchronos/Interop/BlockedCharacterHandler.cs @@ -0,0 +1,42 @@ +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.UI.Info; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Interop; + +public unsafe class BlockedCharacterHandler +{ + private sealed record CharaData(ulong AccId, ulong ContentId); + private readonly Dictionary _blockedCharacterCache = new(); + + private readonly ILogger _logger; + + public BlockedCharacterHandler(ILogger logger, IGameInteropProvider gameInteropProvider) + { + gameInteropProvider.InitializeFromAttributes(this); + _logger = logger; + } + + private static CharaData GetIdsFromPlayerPointer(nint ptr) + { + if (ptr == nint.Zero) return new(0, 0); + var castChar = ((BattleChara*)ptr); + return new(castChar->Character.AccountId, castChar->Character.ContentId); + } + + public bool IsCharacterBlocked(nint ptr, out bool firstTime) + { + firstTime = false; + var combined = GetIdsFromPlayerPointer(ptr); + if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked)) + return isBlocked; + + firstTime = true; + var blockStatus = InfoProxyBlacklist.Instance()->GetBlockResultType(combined.AccId, combined.ContentId); + _logger.LogTrace("CharaPtr {ptr} is BlockStatus: {status}", ptr, blockStatus); + if ((int)blockStatus == 0) + return false; + return _blockedCharacterCache[combined] = blockStatus != InfoProxyBlacklist.BlockResultType.NotBlocked; + } +} diff --git a/MareSynchronos/Interop/DalamudLogger.cs b/MareSynchronos/Interop/DalamudLogger.cs new file mode 100644 index 0000000..277c3dc --- /dev/null +++ b/MareSynchronos/Interop/DalamudLogger.cs @@ -0,0 +1,55 @@ +using Dalamud.Plugin.Services; +using MareSynchronos.MareConfiguration; +using Microsoft.Extensions.Logging; +using System.Text; + +namespace MareSynchronos.Interop; + +internal sealed class DalamudLogger : ILogger +{ + private readonly MareConfigService _mareConfigService; + private readonly string _name; + private readonly IPluginLog _pluginLog; + + public DalamudLogger(string name, MareConfigService mareConfigService, IPluginLog pluginLog) + { + _name = name; + _mareConfigService = mareConfigService; + _pluginLog = pluginLog; + } + + public IDisposable BeginScope(TState state) where TState : notnull => default!; + + public bool IsEnabled(LogLevel logLevel) + { + return (int)_mareConfigService.Current.LogLevel <= (int)logLevel; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) return; + + if ((int)logLevel <= (int)LogLevel.Information) + _pluginLog.Information($"[{_name}]{{{(int)logLevel}}} {state}"); + else + { + StringBuilder sb = new(); + sb.Append($"[{_name}]{{{(int)logLevel}}} {state}: {exception?.Message}"); + if (!string.IsNullOrWhiteSpace(exception?.StackTrace)) + sb.AppendLine(exception?.StackTrace); + var innerException = exception?.InnerException; + while (innerException != null) + { + sb.AppendLine($"InnerException {innerException}: {innerException.Message}"); + sb.AppendLine(innerException.StackTrace); + innerException = innerException.InnerException; + } + if (logLevel == LogLevel.Warning) + _pluginLog.Warning(sb.ToString()); + else if (logLevel == LogLevel.Error) + _pluginLog.Error(sb.ToString()); + else + _pluginLog.Fatal(sb.ToString()); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Interop/DalamudLoggingProvider.cs b/MareSynchronos/Interop/DalamudLoggingProvider.cs new file mode 100644 index 0000000..5ee0eeb --- /dev/null +++ b/MareSynchronos/Interop/DalamudLoggingProvider.cs @@ -0,0 +1,44 @@ +using Dalamud.Plugin.Services; +using MareSynchronos.MareConfiguration; +using Microsoft.Extensions.Logging; + +using System.Collections.Concurrent; + +namespace MareSynchronos.Interop; + +[ProviderAlias("Dalamud")] +public sealed class DalamudLoggingProvider : ILoggerProvider +{ + private readonly ConcurrentDictionary _loggers = + new(StringComparer.OrdinalIgnoreCase); + + private readonly MareConfigService _mareConfigService; + private readonly IPluginLog _pluginLog; + + public DalamudLoggingProvider(MareConfigService mareConfigService, IPluginLog pluginLog) + { + _mareConfigService = mareConfigService; + _pluginLog = pluginLog; + } + + public ILogger CreateLogger(string categoryName) + { + string catName = categoryName.Split(".", StringSplitOptions.RemoveEmptyEntries).Last(); + if (catName.Length > 15) + { + catName = string.Join("", catName.Take(6)) + "..." + string.Join("", catName.TakeLast(6)); + } + else + { + catName = string.Join("", Enumerable.Range(0, 15 - catName.Length).Select(_ => " ")) + catName; + } + + return _loggers.GetOrAdd(catName, name => new DalamudLogger(name, _mareConfigService, _pluginLog)); + } + + public void Dispose() + { + _loggers.Clear(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs b/MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs new file mode 100644 index 0000000..392e5d0 --- /dev/null +++ b/MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs @@ -0,0 +1,20 @@ +using Dalamud.Plugin.Services; +using MareSynchronos.MareConfiguration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Interop; + +public static class DalamudLoggingProviderExtensions +{ + public static ILoggingBuilder AddDalamudLogging(this ILoggingBuilder builder, IPluginLog pluginLog) + { + builder.ClearProviders(); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton + (b => new DalamudLoggingProvider(b.GetRequiredService(), pluginLog))); + + return builder; + } +} \ No newline at end of file diff --git a/MareSynchronos/Interop/GameChatHooks.cs b/MareSynchronos/Interop/GameChatHooks.cs new file mode 100644 index 0000000..02c5a22 --- /dev/null +++ b/MareSynchronos/Interop/GameChatHooks.cs @@ -0,0 +1,333 @@ +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Hooking; +using Dalamud.Memory; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using FFXIVClientStructs.FFXIV.Client.UI.Shell; +using FFXIVClientStructs.FFXIV.Component.Shell; +using MareSynchronos.Services; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Interop; + +public record ChatChannelOverride +{ + public string ChannelName = string.Empty; + public Action? ChatMessageHandler; +} + +public unsafe sealed class GameChatHooks : IDisposable +{ + // Based on https://git.anna.lgbt/anna/ExtraChat/src/branch/main/client/ExtraChat/GameFunctions.cs + + private readonly ILogger _logger; + private readonly Action _ssCommandHandler; + + #region signatures + #pragma warning disable CS0649 + // I do not know what kind of black magic this function performs + // Client::UI::Misc::PronounModule::??? + [Signature("E8 ?? ?? ?? ?? 44 88 74 24 ?? 4C 8D 45")] + private readonly delegate* unmanaged _processStringStep2; + + // Component::Shell::ShellCommandModule::ExecuteCommandInner + private delegate void SendMessageDelegate(ShellCommandModule* module, Utf8String* message, UIModule* uiModule); + [Signature( + "E8 ?? ?? ?? ?? FE 87 ?? ?? ?? ?? C7 87", + DetourName = nameof(SendMessageDetour) + )] + private Hook? SendMessageHook { get; init; } + + // Client::UI::Shell::RaptureShellModule::SetChatChannel + private delegate void SetChatChannelDelegate(RaptureShellModule* module, uint channel); + [Signature( + "E8 ?? ?? ?? ?? 33 C0 EB ?? 85 D2", + DetourName = nameof(SetChatChannelDetour) + )] + private Hook? SetChatChannelHook { get; init; } + + // Component::Shell::ShellCommandModule::ChangeChannelName + private delegate byte* ChangeChannelNameDelegate(AgentChatLog* agent); + [Signature( + "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8D 4D B0 48 8B F8 E8 ?? ?? ?? ?? 41 8B D6", + DetourName = nameof(ChangeChannelNameDetour) + )] + private Hook? ChangeChannelNameHook { get; init; } + + // Client::UI::Agent::AgentChatLog::??? + private delegate byte ShouldDoNameLookupDelegate(AgentChatLog* agent); + [Signature( + "48 89 5C 24 ?? 57 48 83 EC ?? 48 8B D9 40 32 FF 48 8B 49 ?? ?? ?? ?? FF 50", + DetourName = nameof(ShouldDoNameLookupDetour) + )] + private Hook? ShouldDoNameLookupHook { get; init; } + + // Temporary chat channel change (via hotkey) + // Client::UI::Shell::RaptureShellModule::??? + private delegate ulong TempChatChannelDelegate(RaptureShellModule* module, uint x, uint y, ulong z); + [Signature( + "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 83 B9 ?? ?? ?? ?? ?? 49 8B F9 41 8B F0", + DetourName = nameof(TempChatChannelDetour) + )] + private Hook? TempChatChannelHook { get; init; } + + // Temporary tell target change (via hotkey) + // Client::UI::Shell::RaptureShellModule::SetContextTellTargetInForay + private delegate ulong TempTellTargetDelegate(RaptureShellModule* module, ulong a, ulong b, ulong c, ushort d, ulong e, ulong f, ushort g); + [Signature( + "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 83 B9 ?? ?? ?? ?? ?? 41 0F B7 F9", + DetourName = nameof(TempTellTargetDetour) + )] + private Hook? TempTellTargetHook { get; init; } + + // Called every frame while the chat bar is not focused + private delegate void UnfocusTickDelegate(RaptureShellModule* module); + [Signature( + "40 53 48 83 EC ?? 83 B9 ?? ?? ?? ?? ?? 48 8B D9 0F 84 ?? ?? ?? ?? 48 8D 91", + DetourName = nameof(UnfocusTickDetour) + )] + private Hook? UnfocusTickHook { get; init; } + #pragma warning restore CS0649 + #endregion + + private ChatChannelOverride? _chatChannelOverride; + private ChatChannelOverride? _chatChannelOverrideTempBuffer; + private bool _shouldForceNameLookup = false; + + private DateTime _nextMessageIsReply = DateTime.UnixEpoch; + + public ChatChannelOverride? ChatChannelOverride + { + get => _chatChannelOverride; + set { + _chatChannelOverride = value; + _shouldForceNameLookup = true; + } + } + + private void StashChatChannel() + { + if (_chatChannelOverride != null) + { + _logger.LogTrace("Stashing chat channel"); + _chatChannelOverrideTempBuffer = _chatChannelOverride; + ChatChannelOverride = null; + } + } + + private void UnstashChatChannel() + { + if (_chatChannelOverrideTempBuffer != null) + { + _logger.LogTrace("Unstashing chat channel"); + ChatChannelOverride = _chatChannelOverrideTempBuffer; + _chatChannelOverrideTempBuffer = null; + } + } + + public GameChatHooks(ILogger logger, IGameInteropProvider gameInteropProvider, Action ssCommandHandler) + { + _logger = logger; + _ssCommandHandler = ssCommandHandler; + + logger.LogInformation("Initializing GameChatHooks"); + gameInteropProvider.InitializeFromAttributes(this); + + SendMessageHook?.Enable(); + SetChatChannelHook?.Enable(); + ChangeChannelNameHook?.Enable(); + ShouldDoNameLookupHook?.Enable(); + TempChatChannelHook?.Enable(); + TempTellTargetHook?.Enable(); + UnfocusTickHook?.Enable(); + } + + public void Dispose() + { + SendMessageHook?.Dispose(); + SetChatChannelHook?.Dispose(); + ChangeChannelNameHook?.Dispose(); + ShouldDoNameLookupHook?.Dispose(); + TempChatChannelHook?.Dispose(); + TempTellTargetHook?.Dispose(); + UnfocusTickHook?.Dispose(); + } + + private byte[] ProcessChatMessage(Utf8String* message) + { + var pronounModule = UIModule.Instance()->GetPronounModule(); + var chatString1 = pronounModule->ProcessString(message, true); + var chatString2 = _processStringStep2(pronounModule, chatString1, 1); + return MemoryHelper.ReadRaw((nint)chatString2->StringPtr.Value, chatString2->Length); + } + + private void SendMessageDetour(ShellCommandModule* thisPtr, Utf8String* message, UIModule* uiModule) + { + try + { + var messageLength = message->Length; + var messageSpan = message->AsSpan(); + + bool isCommand = false; + bool isReply = false; + + var utcNow = DateTime.UtcNow; + + // Check if chat input begins with a command (or auto-translated command) + // Or if we think we're being called to send text via the /r command + if (_nextMessageIsReply >= utcNow) + { + isCommand = true; + } + else if (messageLength == 0 || messageSpan[0] == (byte)'/' || !messageSpan.ContainsAnyExcept((byte)' ')) + { + isCommand = true; + if (messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes("/r ")) || messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes("/reply "))) + isReply = true; + } + else if (messageSpan[0] == (byte)0x02) /* Payload.START_BYTE */ + { + var payload = Payload.Decode(new BinaryReader(new UnmanagedMemoryStream(message->StringPtr, message->BufSize))) as AutoTranslatePayload; + + // Auto-translate text begins with / + if (payload != null && payload.Text.Length > 2 && payload.Text[2] == '/') + { + isCommand = true; + if (payload.Text[2..].StartsWith("/r ", StringComparison.Ordinal) || payload.Text[2..].StartsWith("/reply ", StringComparison.Ordinal)) + isReply = true; + } + } + + // When using /r the game will set a flag and then call this function a second time + // The next call to this function will be raw text intended for the IM recipient + // This flag's validity is time-limited as a fail-safe + if (isReply) + _nextMessageIsReply = utcNow + TimeSpan.FromMilliseconds(100); + + // If it is a command, check if it begins with /ss first so we can handle the message directly + // Letting Dalamud handle the commands causes all of the special payloads to be dropped + if (isCommand && messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes("/ss"))) + { + for (int i = 1; i <= ChatService.CommandMaxNumber; ++i) + { + var cmdString = $"/ss{i} "; + if (messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes(cmdString))) + { + var ssChatBytes = ProcessChatMessage(message); + ssChatBytes = ssChatBytes.Skip(cmdString.Length).ToArray(); + _ssCommandHandler?.Invoke(i, ssChatBytes); + return; + } + } + } + + // If not a command, or no override is set, then call the original chat handler + if (isCommand || _chatChannelOverride == null) + { + SendMessageHook!.OriginalDisposeSafe(thisPtr, message, uiModule); + return; + } + + // Otherwise, the text is to be sent to the emulated chat channel handler + // The chat input string is rendered in to a payload for display first + var chatBytes = ProcessChatMessage(message); + + if (chatBytes.Length > 0) + _chatChannelOverride.ChatMessageHandler?.Invoke(chatBytes); + } + catch (Exception e) + { + _logger.LogError(e, "Exception thrown during SendMessageDetour"); + } + } + + private void SetChatChannelDetour(RaptureShellModule* module, uint channel) + { + try + { + if (_chatChannelOverride != null) + { + _chatChannelOverride = null; + _shouldForceNameLookup = true; + } + } + catch (Exception e) + { + _logger.LogError(e, "Exception thrown during SetChatChannelDetour"); + } + + SetChatChannelHook!.OriginalDisposeSafe(module, channel); + } + + private ulong TempChatChannelDetour(RaptureShellModule* module, uint x, uint y, ulong z) + { + var result = TempChatChannelHook!.OriginalDisposeSafe(module, x, y, z); + + if (result != 0) + StashChatChannel(); + + return result; + } + + private ulong TempTellTargetDetour(RaptureShellModule* module, ulong a, ulong b, ulong c, ushort d, ulong e, ulong f, ushort g) + { + var result = TempTellTargetHook!.OriginalDisposeSafe(module, a, b, c, d, e, f, g); + + if (result != 0) + StashChatChannel(); + + return result; + } + + private void UnfocusTickDetour(RaptureShellModule* module) + { + UnfocusTickHook!.OriginalDisposeSafe(module); + UnstashChatChannel(); + } + + private byte* ChangeChannelNameDetour(AgentChatLog* agent) + { + var originalResult = ChangeChannelNameHook!.OriginalDisposeSafe(agent); + + try + { + // Replace the chat channel name on the UI if active + if (_chatChannelOverride != null) + { + agent->ChannelLabel.SetString(_chatChannelOverride.ChannelName); + } + } + catch (Exception e) + { + _logger.LogError(e, "Exception thrown during ChangeChannelNameDetour"); + } + + return originalResult; + } + + private byte ShouldDoNameLookupDetour(AgentChatLog* agent) + { + var originalResult = ShouldDoNameLookupHook!.OriginalDisposeSafe(agent); + + try + { + // Force the chat channel name to update when required + if (_shouldForceNameLookup) + { + _shouldForceNameLookup = false; + return 1; + } + } + catch (Exception e) + { + _logger.LogError(e, "Exception thrown during ShouldDoNameLookupDetour"); + } + + return originalResult; + } +} diff --git a/MareSynchronos/Interop/GameModel/MdlFile.cs b/MareSynchronos/Interop/GameModel/MdlFile.cs new file mode 100644 index 0000000..c77d5e3 --- /dev/null +++ b/MareSynchronos/Interop/GameModel/MdlFile.cs @@ -0,0 +1,259 @@ +using Lumina.Data; +using Lumina.Extensions; +using System.Runtime.InteropServices; +using System.Text; +using static Lumina.Data.Parsing.MdlStructs; + +namespace MareSynchronos.Interop.GameModel; + +#pragma warning disable S1104 // Fields should not have public accessibility + +// This code is completely and shamelessly borrowed from Penumbra to load V5 and V6 model files. +// Original Source: https://github.com/Ottermandias/Penumbra.GameData/blob/main/Files/MdlFile.cs +public class MdlFile +{ + public const int V5 = 0x01000005; + public const int V6 = 0x01000006; + public const uint NumVertices = 17; + public const uint FileHeaderSize = 0x44; + + // Raw data to write back. + public uint Version = 0x01000005; + public float Radius; + public float ModelClipOutDistance; + public float ShadowClipOutDistance; + public byte BgChangeMaterialIndex; + public byte BgCrestChangeMaterialIndex; + public ushort CullingGridCount; + public byte Flags3; + public byte Unknown6; + public ushort Unknown8; + public ushort Unknown9; + + // Offsets are stored relative to RuntimeSize instead of file start. + public uint[] VertexOffset = [0, 0, 0]; + public uint[] IndexOffset = [0, 0, 0]; + + public uint[] VertexBufferSize = [0, 0, 0]; + public uint[] IndexBufferSize = [0, 0, 0]; + public byte LodCount; + public bool EnableIndexBufferStreaming; + public bool EnableEdgeGeometry; + + public ModelFlags1 Flags1; + public ModelFlags2 Flags2; + + public VertexDeclarationStruct[] VertexDeclarations = []; + public ElementIdStruct[] ElementIds = []; + public MeshStruct[] Meshes = []; + public BoundingBoxStruct[] BoneBoundingBoxes = []; + public LodStruct[] Lods = []; + public ExtraLodStruct[] ExtraLods = []; + + public MdlFile(string filePath) + { + using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + using var r = new LuminaBinaryReader(stream); + + var header = LoadModelFileHeader(r); + LodCount = header.LodCount; + VertexBufferSize = header.VertexBufferSize; + IndexBufferSize = header.IndexBufferSize; + VertexOffset = header.VertexOffset; + IndexOffset = header.IndexOffset; + + var dataOffset = FileHeaderSize + header.RuntimeSize + header.StackSize; + for (var i = 0; i < LodCount; ++i) + { + VertexOffset[i] -= dataOffset; + IndexOffset[i] -= dataOffset; + } + + VertexDeclarations = new VertexDeclarationStruct[header.VertexDeclarationCount]; + for (var i = 0; i < header.VertexDeclarationCount; ++i) + VertexDeclarations[i] = VertexDeclarationStruct.Read(r); + + _ = LoadStrings(r); + + var modelHeader = LoadModelHeader(r); + ElementIds = new ElementIdStruct[modelHeader.ElementIdCount]; + for (var i = 0; i < modelHeader.ElementIdCount; i++) + ElementIds[i] = ElementIdStruct.Read(r); + + Lods = new LodStruct[3]; + for (var i = 0; i < 3; i++) + { + var lod = r.ReadStructure(); + if (i < LodCount) + { + lod.VertexDataOffset -= dataOffset; + lod.IndexDataOffset -= dataOffset; + } + + Lods[i] = lod; + } + + ExtraLods = modelHeader.Flags2.HasFlag(ModelFlags2.ExtraLodEnabled) + ? r.ReadStructuresAsArray(3) + : []; + + Meshes = new MeshStruct[modelHeader.MeshCount]; + for (var i = 0; i < modelHeader.MeshCount; i++) + Meshes[i] = MeshStruct.Read(r); + } + + private ModelFileHeader LoadModelFileHeader(LuminaBinaryReader r) + { + var header = ModelFileHeader.Read(r); + Version = header.Version; + EnableIndexBufferStreaming = header.EnableIndexBufferStreaming; + EnableEdgeGeometry = header.EnableEdgeGeometry; + return header; + } + + private ModelHeader LoadModelHeader(BinaryReader r) + { + var modelHeader = r.ReadStructure(); + Radius = modelHeader.Radius; + Flags1 = modelHeader.Flags1; + Flags2 = modelHeader.Flags2; + ModelClipOutDistance = modelHeader.ModelClipOutDistance; + ShadowClipOutDistance = modelHeader.ShadowClipOutDistance; + CullingGridCount = modelHeader.CullingGridCount; + Flags3 = modelHeader.Flags3; + Unknown6 = modelHeader.Unknown6; + Unknown8 = modelHeader.Unknown8; + Unknown9 = modelHeader.Unknown9; + BgChangeMaterialIndex = modelHeader.BGChangeMaterialIndex; + BgCrestChangeMaterialIndex = modelHeader.BGCrestChangeMaterialIndex; + + return modelHeader; + } + + private static (uint[], string[]) LoadStrings(BinaryReader r) + { + var stringCount = r.ReadUInt16(); + r.ReadUInt16(); + var stringSize = (int)r.ReadUInt32(); + var stringData = r.ReadBytes(stringSize); + var start = 0; + var strings = new string[stringCount]; + var offsets = new uint[stringCount]; + for (var i = 0; i < stringCount; ++i) + { + var span = stringData.AsSpan(start); + var idx = span.IndexOf((byte)'\0'); + strings[i] = Encoding.UTF8.GetString(span[..idx]); + offsets[i] = (uint)start; + start = start + idx + 1; + } + + return (offsets, strings); + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct ModelHeader + { + // MeshHeader + public float Radius; + public ushort MeshCount; + public ushort AttributeCount; + public ushort SubmeshCount; + public ushort MaterialCount; + public ushort BoneCount; + public ushort BoneTableCount; + public ushort ShapeCount; + public ushort ShapeMeshCount; + public ushort ShapeValueCount; + public byte LodCount; + public ModelFlags1 Flags1; + public ushort ElementIdCount; + public byte TerrainShadowMeshCount; + public ModelFlags2 Flags2; + public float ModelClipOutDistance; + public float ShadowClipOutDistance; + public ushort CullingGridCount; + public ushort TerrainShadowSubmeshCount; + public byte Flags3; + public byte BGChangeMaterialIndex; + public byte BGCrestChangeMaterialIndex; + public byte Unknown6; + public ushort BoneTableArrayCountTotal; + public ushort Unknown8; + public ushort Unknown9; + private fixed byte _padding[6]; + } + + public struct ShapeStruct + { + public uint StringOffset; + public ushort[] ShapeMeshStartIndex; + public ushort[] ShapeMeshCount; + + public static ShapeStruct Read(LuminaBinaryReader br) + { + ShapeStruct ret = new ShapeStruct(); + ret.StringOffset = br.ReadUInt32(); + ret.ShapeMeshStartIndex = br.ReadUInt16Array(3); + ret.ShapeMeshCount = br.ReadUInt16Array(3); + return ret; + } + } + + [Flags] + public enum ModelFlags1 : byte + { + DustOcclusionEnabled = 0x80, + SnowOcclusionEnabled = 0x40, + RainOcclusionEnabled = 0x20, + Unknown1 = 0x10, + LightingReflectionEnabled = 0x08, + WavingAnimationDisabled = 0x04, + LightShadowDisabled = 0x02, + ShadowDisabled = 0x01, + } + + [Flags] + public enum ModelFlags2 : byte + { + Unknown2 = 0x80, + BgUvScrollEnabled = 0x40, + EnableForceNonResident = 0x20, + ExtraLodEnabled = 0x10, + ShadowMaskEnabled = 0x08, + ForceLodRangeEnabled = 0x04, + EdgeGeometryEnabled = 0x02, + Unknown3 = 0x01 + } + + public struct VertexDeclarationStruct + { + // There are always 17, but stop when stream = -1 + public VertexElement[] VertexElements; + + public static VertexDeclarationStruct Read(LuminaBinaryReader br) + { + VertexDeclarationStruct ret = new VertexDeclarationStruct(); + + var elems = new List(); + + // Read the vertex elements that we need + var thisElem = br.ReadStructure(); + do + { + elems.Add(thisElem); + thisElem = br.ReadStructure(); + } while (thisElem.Stream != 255); + + // Skip the number of bytes that we don't need to read + // We skip elems.Count * 9 because we had to read the invalid element + int toSeek = 17 * 8 - (elems.Count + 1) * 8; + br.Seek(br.BaseStream.Position + toSeek); + + ret.VertexElements = elems.ToArray(); + + return ret; + } + } +} +#pragma warning restore S1104 // Fields should not have public accessibility \ No newline at end of file diff --git a/MareSynchronos/Interop/Ipc/IIpcCaller.cs b/MareSynchronos/Interop/Ipc/IIpcCaller.cs new file mode 100644 index 0000000..faa993a --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IIpcCaller.cs @@ -0,0 +1,7 @@ +namespace MareSynchronos.Interop.Ipc; + +public interface IIpcCaller : IDisposable +{ + bool APIAvailable { get; } + void CheckAPI(); +} diff --git a/MareSynchronos/Interop/Ipc/IpcCallerBrio.cs b/MareSynchronos/Interop/Ipc/IpcCallerBrio.cs new file mode 100644 index 0000000..b8a9c58 --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcCallerBrio.cs @@ -0,0 +1,147 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.Services; +using Microsoft.Extensions.Logging; +using System.Numerics; +using System.Text.Json.Nodes; + +namespace MareSynchronos.Interop.Ipc; + +public sealed class IpcCallerBrio : IIpcCaller +{ + 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; + + + public bool APIAvailable { get; private set; } + + public IpcCallerBrio(ILogger logger, IDalamudPluginInterface dalamudPluginInterface, + DalamudUtilService dalamudUtilService) + { + _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"); + + 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); + } + + public async Task DespawnActorAsync(nint address) + { + if (!APIAvailable) return false; + 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); + } + + public async Task ApplyTransformAsync(nint address, WorldData data) + { + if (!APIAvailable) return false; + var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false); + if (gameObject == null) return false; + _logger.LogDebug("Applying Transform to Actor {actor}", gameObject.Name.TextValue); + + return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetModelTransform.InvokeFunc(gameObject, + new Vector3(data.PositionX, data.PositionY, data.PositionZ), + new Quaternion(data.RotationX, data.RotationY, data.RotationZ, data.RotationW), + new Vector3(data.ScaleX, data.ScaleY, data.ScaleZ), false)).ConfigureAwait(false); + } + + public async Task GetTransformAsync(nint address) + { + 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); + if (data.Item1 == null || data.Item2 == null || data.Item3 == null) return default; + //_logger.LogDebug("Getting Transform from Actor {actor}", gameObject.Name.TextValue); + + return new WorldData() + { + PositionX = data.Item1.Value.X, + PositionY = data.Item1.Value.Y, + PositionZ = data.Item1.Value.Z, + RotationX = data.Item2.Value.X, + RotationY = data.Item2.Value.Y, + RotationZ = data.Item2.Value.Z, + RotationW = data.Item2.Value.W, + ScaleX = data.Item3.Value.X, + ScaleY = data.Item3.Value.Y, + ScaleZ = data.Item3.Value.Z + }; + } + + public async Task GetPoseAsync(nint address) + { + if (!APIAvailable) return null; + var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false); + if (gameObject == null) return null; + _logger.LogDebug("Getting Pose from Actor {actor}", gameObject.Name.TextValue); + + return await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false); + } + + public async Task SetPoseAsync(nint address, string pose) + { + if (!APIAvailable) return false; + var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false); + if (gameObject == null) return false; + _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); + applicablePose["ModelDifference"] = JsonNode.Parse(JsonNode.Parse(currentPose)!["ModelDifference"]!.ToJsonString()); + + await _dalamudUtilService.RunOnFrameworkThread(() => + { + _brioFreezeActor.InvokeFunc(gameObject); + _brioFreezePhysics.InvokeFunc(); + }).ConfigureAwait(false); + return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetPoseFromJson.InvokeFunc(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false); + } + + public void Dispose() + { + } +} diff --git a/MareSynchronos/Interop/Ipc/IpcCallerCustomize.cs b/MareSynchronos/Interop/Ipc/IpcCallerCustomize.cs new file mode 100644 index 0000000..5029a14 --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcCallerCustomize.cs @@ -0,0 +1,139 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Dalamud.Utility; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; +using System.Text; + +namespace MareSynchronos.Interop.Ipc; + +public sealed class IpcCallerCustomize : IIpcCaller +{ + private readonly ICallGateSubscriber<(int, int)> _customizePlusApiVersion; + private readonly ICallGateSubscriber _customizePlusGetActiveProfile; + private readonly ICallGateSubscriber _customizePlusGetProfileById; + private readonly ICallGateSubscriber _customizePlusOnScaleUpdate; + private readonly ICallGateSubscriber _customizePlusRevertCharacter; + private readonly ICallGateSubscriber _customizePlusSetBodyScaleToCharacter; + private readonly ICallGateSubscriber _customizePlusDeleteByUniqueId; + private readonly ILogger _logger; + private readonly DalamudUtilService _dalamudUtil; + private readonly MareMediator _mareMediator; + + public IpcCallerCustomize(ILogger logger, IDalamudPluginInterface dalamudPluginInterface, + DalamudUtilService dalamudUtil, MareMediator mareMediator) + { + _customizePlusApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("CustomizePlus.General.GetApiVersion"); + _customizePlusGetActiveProfile = dalamudPluginInterface.GetIpcSubscriber("CustomizePlus.Profile.GetActiveProfileIdOnCharacter"); + _customizePlusGetProfileById = dalamudPluginInterface.GetIpcSubscriber("CustomizePlus.Profile.GetByUniqueId"); + _customizePlusRevertCharacter = dalamudPluginInterface.GetIpcSubscriber("CustomizePlus.Profile.DeleteTemporaryProfileOnCharacter"); + _customizePlusSetBodyScaleToCharacter = dalamudPluginInterface.GetIpcSubscriber("CustomizePlus.Profile.SetTemporaryProfileOnCharacter"); + _customizePlusOnScaleUpdate = dalamudPluginInterface.GetIpcSubscriber("CustomizePlus.Profile.OnUpdate"); + _customizePlusDeleteByUniqueId = dalamudPluginInterface.GetIpcSubscriber("CustomizePlus.Profile.DeleteTemporaryProfileByUniqueId"); + + _customizePlusOnScaleUpdate.Subscribe(OnCustomizePlusScaleChange); + _logger = logger; + _dalamudUtil = dalamudUtil; + _mareMediator = mareMediator; + + CheckAPI(); + } + + public bool APIAvailable { get; private set; } = false; + + public async Task RevertAsync(nint character) + { + if (!APIAvailable) return; + await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is ICharacter c) + { + _logger.LogTrace("CustomizePlus reverting for {chara}", c.Address.ToString("X")); + _customizePlusRevertCharacter!.InvokeFunc(c.ObjectIndex); + } + }).ConfigureAwait(false); + } + + public async Task SetBodyScaleAsync(nint character, string scale) + { + if (!APIAvailable) return null; + return await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is ICharacter c) + { + string decodedScale = Encoding.UTF8.GetString(Convert.FromBase64String(scale)); + _logger.LogTrace("CustomizePlus applying for {chara}", c.Address.ToString("X")); + if (scale.IsNullOrEmpty()) + { + _customizePlusRevertCharacter!.InvokeFunc(c.ObjectIndex); + return null; + } + else + { + var result = _customizePlusSetBodyScaleToCharacter!.InvokeFunc(c.ObjectIndex, decodedScale); + return result.Item2; + } + } + + return null; + }).ConfigureAwait(false); + } + + public async Task RevertByIdAsync(Guid? profileId) + { + if (!APIAvailable || profileId == null) return; + + await _dalamudUtil.RunOnFrameworkThread(() => + { + _ = _customizePlusDeleteByUniqueId.InvokeFunc(profileId.Value); + }).ConfigureAwait(false); + } + + public async Task GetScaleAsync(nint character) + { + if (!APIAvailable) return null; + var scale = await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is ICharacter c) + { + var res = _customizePlusGetActiveProfile.InvokeFunc(c.ObjectIndex); + _logger.LogTrace("CustomizePlus GetActiveProfile returned {err}", res.Item1); + if (res.Item1 != 0 || res.Item2 == null) return string.Empty; + return _customizePlusGetProfileById.InvokeFunc(res.Item2.Value).Item2; + } + + return string.Empty; + }).ConfigureAwait(false); + if (string.IsNullOrEmpty(scale)) return string.Empty; + return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale)); + } + + public void CheckAPI() + { + try + { + var version = _customizePlusApiVersion.InvokeFunc(); + APIAvailable = (version.Item1 == 6 && version.Item2 >= 0); + } + catch + { + APIAvailable = false; + } + } + + private void OnCustomizePlusScaleChange(ushort c, Guid g) + { + var obj = _dalamudUtil.GetCharacterFromObjectTableByIndex(c); + _mareMediator.Publish(new CustomizePlusMessage(obj?.Address ?? null)); + } + + public void Dispose() + { + _customizePlusOnScaleUpdate.Unsubscribe(OnCustomizePlusScaleChange); + } +} diff --git a/MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs b/MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs new file mode 100644 index 0000000..f2603de --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs @@ -0,0 +1,253 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using Glamourer.Api.Helpers; +using Glamourer.Api.IpcSubscribers; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Interop.Ipc; + +public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcCaller +{ + private readonly ILogger _logger; + private readonly DalamudUtilService _dalamudUtil; + private readonly MareMediator _mareMediator; + private readonly RedrawManager _redrawManager; + + private readonly ApiVersion _glamourerApiVersions; + private readonly ApplyState? _glamourerApplyAll; + private readonly GetStateBase64? _glamourerGetAllCustomization; + private readonly RevertState _glamourerRevert; + private readonly RevertStateName _glamourerRevertByName; + private readonly UnlockState _glamourerUnlock; + private readonly UnlockStateName _glamourerUnlockByName; + private readonly EventSubscriber? _glamourerStateChanged; + + private bool _pluginLoaded; + private Version _pluginVersion; + + private bool _shownGlamourerUnavailable = false; + private readonly uint LockCode = 0x626E7579; + + public IpcCallerGlamourer(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, MareMediator mareMediator, + RedrawManager redrawManager) : base(logger, mareMediator) + { + _glamourerApiVersions = new ApiVersion(pi); + _glamourerGetAllCustomization = new GetStateBase64(pi); + _glamourerApplyAll = new ApplyState(pi); + _glamourerRevert = new RevertState(pi); + _glamourerRevertByName = new RevertStateName(pi); + _glamourerUnlock = new UnlockState(pi); + _glamourerUnlockByName = new UnlockStateName(pi); + + _logger = logger; + _dalamudUtil = dalamudUtil; + _mareMediator = mareMediator; + _redrawManager = redrawManager; + + var plugin = PluginWatcherService.GetInitialPluginState(pi, "Glamourer"); + + _pluginLoaded = plugin?.IsLoaded ?? false; + _pluginVersion = plugin?.Version ?? new(0, 0, 0, 0); + + Mediator.SubscribeKeyed(this, "Glamourer", (msg) => + { + _pluginLoaded = msg.IsLoaded; + _pluginVersion = msg.Version; + CheckAPI(); + }); + + CheckAPI(); + + _glamourerStateChanged = StateChanged.Subscriber(pi, GlamourerChanged); + _glamourerStateChanged.Enable(); + + Mediator.Subscribe(this, s => _shownGlamourerUnavailable = false); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _redrawManager.Cancel(); + _glamourerStateChanged?.Dispose(); + } + + public bool APIAvailable { get; private set; } + + public void CheckAPI() + { + bool apiAvailable = false; + try + { + bool versionValid = _pluginLoaded && _pluginVersion >= new Version(1, 0, 6, 1); + 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; + _mareMediator.Publish(new NotificationMessage("Glamourer inactive", "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Snowcloak. 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 allowImmediate = false) + { + if (!APIAvailable || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return; + + // Call immediately if possible + if (allowImmediate && _dalamudUtil.IsOnFrameworkThread && !await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) + { + var gameObj = await _dalamudUtil.CreateGameObjectAsync(handler.Address).ConfigureAwait(false); + if (gameObj is ICharacter chara) + { + logger.LogDebug("[{appid}] Calling on IPC: GlamourerApplyAll", applicationId); + _glamourerApplyAll!.Invoke(customization, chara.ObjectIndex, LockCode); + return; + } + } + + await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false); + + try + { + await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) => + { + try + { + logger.LogDebug("[{appid}] Calling on IPC: GlamourerApplyAll", applicationId); + _glamourerApplyAll!.Invoke(customization, chara.ObjectIndex, LockCode); + } + catch (Exception ex) + { + logger.LogWarning(ex, "[{appid}] Failed to apply Glamourer data", applicationId); + } + }, token).ConfigureAwait(false); + } + finally + { + _redrawManager.RedrawSemaphore.Release(); + } + } + + public async Task GetCharacterCustomizationAsync(IntPtr character) + { + if (!APIAvailable) return string.Empty; + try + { + return await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is ICharacter c) + { + return _glamourerGetAllCustomization!.Invoke(c.ObjectIndex).Item2 ?? string.Empty; + } + return string.Empty; + }).ConfigureAwait(false); + } + catch + { + return string.Empty; + } + } + + public async Task RevertAsync(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) => + { + try + { + logger.LogDebug("[{appid}] Calling On IPC: GlamourerUnlock", applicationId); + _glamourerUnlock.Invoke(chara.ObjectIndex, LockCode); + logger.LogDebug("[{appid}] Calling On IPC: GlamourerRevert", applicationId); + _glamourerRevert.Invoke(chara.ObjectIndex, LockCode); + logger.LogDebug("[{appid}] Calling On IPC: PenumbraRedraw", applicationId); + _mareMediator.Publish(new PenumbraRedrawCharacterMessage(chara)); + } + catch (Exception ex) + { + logger.LogWarning(ex, "[{appid}] Error during GlamourerRevert", applicationId); + } + }, token).ConfigureAwait(false); + } + finally + { + _redrawManager.RedrawSemaphore.Release(); + } + } + + public void RevertNow(ILogger logger, Guid applicationId, int objectIndex) + { + if ((!APIAvailable) || _dalamudUtil.IsZoning) return; + logger.LogTrace("[{applicationId}] Immediately reverting object index {objId}", applicationId, objectIndex); + _glamourerRevert.Invoke(objectIndex, LockCode); + } + + public void RevertByNameNow(ILogger logger, Guid applicationId, string name) + { + if ((!APIAvailable) || _dalamudUtil.IsZoning) return; + logger.LogTrace("[{applicationId}] Immediately reverting {name}", applicationId, name); + _glamourerRevertByName.Invoke(name, LockCode); + } + + public async Task RevertByNameAsync(ILogger logger, string name, Guid applicationId) + { + if ((!APIAvailable) || _dalamudUtil.IsZoning) return; + + await _dalamudUtil.RunOnFrameworkThread(() => + { + RevertByName(logger, name, applicationId); + + }).ConfigureAwait(false); + } + + public void RevertByName(ILogger logger, string name, Guid applicationId) + { + if ((!APIAvailable) || _dalamudUtil.IsZoning) return; + + try + { + logger.LogDebug("[{appid}] Calling On IPC: GlamourerRevertByName", applicationId); + _glamourerRevertByName.Invoke(name, LockCode); + logger.LogDebug("[{appid}] Calling On IPC: GlamourerUnlockName", applicationId); + _glamourerUnlockByName.Invoke(name, LockCode); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during Glamourer RevertByName"); + } + } + + private void GlamourerChanged(nint address) + { + _mareMediator.Publish(new GlamourerChangedMessage(address)); + } +} diff --git a/MareSynchronos/Interop/Ipc/IpcCallerHeels.cs b/MareSynchronos/Interop/Ipc/IpcCallerHeels.cs new file mode 100644 index 0000000..994b73c --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcCallerHeels.cs @@ -0,0 +1,93 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Interop.Ipc; + +public sealed class IpcCallerHeels : IIpcCaller +{ + private readonly ILogger _logger; + private readonly MareMediator _mareMediator; + private readonly DalamudUtilService _dalamudUtil; + private readonly ICallGateSubscriber<(int, int)> _heelsGetApiVersion; + private readonly ICallGateSubscriber _heelsGetOffset; + private readonly ICallGateSubscriber _heelsOffsetUpdate; + private readonly ICallGateSubscriber _heelsRegisterPlayer; + private readonly ICallGateSubscriber _heelsUnregisterPlayer; + + public IpcCallerHeels(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, MareMediator mareMediator) + { + _logger = logger; + _mareMediator = mareMediator; + _dalamudUtil = dalamudUtil; + _heelsGetApiVersion = pi.GetIpcSubscriber<(int, int)>("SimpleHeels.ApiVersion"); + _heelsGetOffset = pi.GetIpcSubscriber("SimpleHeels.GetLocalPlayer"); + _heelsRegisterPlayer = pi.GetIpcSubscriber("SimpleHeels.RegisterPlayer"); + _heelsUnregisterPlayer = pi.GetIpcSubscriber("SimpleHeels.UnregisterPlayer"); + _heelsOffsetUpdate = pi.GetIpcSubscriber("SimpleHeels.LocalChanged"); + + _heelsOffsetUpdate.Subscribe(HeelsOffsetChange); + + CheckAPI(); + } + + public bool APIAvailable { get; private set; } = false; + + private void HeelsOffsetChange(string offset) + { + _mareMediator.Publish(new HeelsOffsetMessage()); + } + + public async Task GetOffsetAsync() + { + if (!APIAvailable) return string.Empty; + return await _dalamudUtil.RunOnFrameworkThread(_heelsGetOffset.InvokeFunc).ConfigureAwait(false); + } + + public async Task RestoreOffsetForPlayerAsync(IntPtr character) + { + if (!APIAvailable) return; + await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + _logger.LogTrace("Restoring Heels data to {chara}", character.ToString("X")); + _heelsUnregisterPlayer.InvokeAction(gameObj.ObjectIndex); + } + }).ConfigureAwait(false); + } + + public async Task SetOffsetForPlayerAsync(IntPtr character, string data) + { + if (!APIAvailable) return; + await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + _logger.LogTrace("Applying Heels data to {chara}", character.ToString("X")); + _heelsRegisterPlayer.InvokeAction(gameObj.ObjectIndex, data); + } + }).ConfigureAwait(false); + } + + public void CheckAPI() + { + try + { + APIAvailable = _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 0 }; + } + catch + { + APIAvailable = false; + } + } + + public void Dispose() + { + _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); + } +} diff --git a/MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs b/MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs new file mode 100644 index 0000000..0edb543 --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs @@ -0,0 +1,135 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; +using System.Text; + +namespace MareSynchronos.Interop.Ipc; + +public sealed class IpcCallerHonorific : IIpcCaller +{ + private readonly ICallGateSubscriber<(uint major, uint minor)> _honorificApiVersion; + private readonly ICallGateSubscriber _honorificClearCharacterTitle; + private readonly ICallGateSubscriber _honorificDisposing; + private readonly ICallGateSubscriber _honorificGetLocalCharacterTitle; + private readonly ICallGateSubscriber _honorificLocalCharacterTitleChanged; + private readonly ICallGateSubscriber _honorificReady; + private readonly ICallGateSubscriber _honorificSetCharacterTitle; + private readonly ILogger _logger; + private readonly MareMediator _mareMediator; + private readonly DalamudUtilService _dalamudUtil; + + public IpcCallerHonorific(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, + MareMediator mareMediator) + { + _logger = logger; + _mareMediator = mareMediator; + _dalamudUtil = dalamudUtil; + _honorificApiVersion = pi.GetIpcSubscriber<(uint, uint)>("Honorific.ApiVersion"); + _honorificGetLocalCharacterTitle = pi.GetIpcSubscriber("Honorific.GetLocalCharacterTitle"); + _honorificClearCharacterTitle = pi.GetIpcSubscriber("Honorific.ClearCharacterTitle"); + _honorificSetCharacterTitle = pi.GetIpcSubscriber("Honorific.SetCharacterTitle"); + _honorificLocalCharacterTitleChanged = pi.GetIpcSubscriber("Honorific.LocalCharacterTitleChanged"); + _honorificDisposing = pi.GetIpcSubscriber("Honorific.Disposing"); + _honorificReady = pi.GetIpcSubscriber("Honorific.Ready"); + + _honorificLocalCharacterTitleChanged.Subscribe(OnHonorificLocalCharacterTitleChanged); + _honorificDisposing.Subscribe(OnHonorificDisposing); + _honorificReady.Subscribe(OnHonorificReady); + + CheckAPI(); + } + + public bool APIAvailable { get; private set; } = false; + + public void CheckAPI() + { + try + { + APIAvailable = _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 0 }; + } + catch + { + APIAvailable = false; + } + } + + public void Dispose() + { + _honorificLocalCharacterTitleChanged.Unsubscribe(OnHonorificLocalCharacterTitleChanged); + _honorificDisposing.Unsubscribe(OnHonorificDisposing); + _honorificReady.Unsubscribe(OnHonorificReady); + } + + public async Task ClearTitleAsync(nint character) + { + if (!APIAvailable) return; + await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is IPlayerCharacter c) + { + _logger.LogTrace("Honorific removing for {addr}", c.Address.ToString("X")); + _honorificClearCharacterTitle!.InvokeAction(c.ObjectIndex); + } + }).ConfigureAwait(false); + } + + public async Task GetTitle() + { + if (!APIAvailable) return string.Empty; + return await _dalamudUtil.RunOnFrameworkThread(() => + { + string title = _honorificGetLocalCharacterTitle.InvokeFunc(); + return string.IsNullOrEmpty(title) ? string.Empty : Convert.ToBase64String(Encoding.UTF8.GetBytes(title)); + }).ConfigureAwait(false); + } + + public async Task SetTitleAsync(IntPtr character, string honorificDataB64) + { + if (!APIAvailable) return; + _logger.LogTrace("Applying Honorific data to {chara}", character.ToString("X")); + try + { + await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is IPlayerCharacter pc) + { + string honorificData = string.IsNullOrEmpty(honorificDataB64) ? string.Empty : Encoding.UTF8.GetString(Convert.FromBase64String(honorificDataB64)); + if (string.IsNullOrEmpty(honorificData)) + { + _honorificClearCharacterTitle!.InvokeAction(pc.ObjectIndex); + } + else + { + _honorificSetCharacterTitle!.InvokeAction(pc.ObjectIndex, honorificData); + } + } + }).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogWarning(e, "Could not apply Honorific data"); + } + } + + private void OnHonorificDisposing() + { + _mareMediator.Publish(new HonorificMessage(string.Empty)); + } + + private void OnHonorificLocalCharacterTitleChanged(string titleJson) + { + string titleData = string.IsNullOrEmpty(titleJson) ? string.Empty : Convert.ToBase64String(Encoding.UTF8.GetBytes(titleJson)); + _mareMediator.Publish(new HonorificMessage(titleData)); + } + + private void OnHonorificReady() + { + CheckAPI(); + _mareMediator.Publish(new HonorificReadyMessage()); + } +} diff --git a/MareSynchronos/Interop/Ipc/IpcCallerMare.cs b/MareSynchronos/Interop/Ipc/IpcCallerMare.cs new file mode 100644 index 0000000..ead5487 --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcCallerMare.cs @@ -0,0 +1,44 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Interop.Ipc; + +public sealed class IpcCallerMare : DisposableMediatorSubscriberBase +{ + private readonly ICallGateSubscriber> _mareHandledGameAddresses; + private readonly List _emptyList = []; + + private bool _pluginLoaded; + + public IpcCallerMare(ILogger logger, IDalamudPluginInterface pi, MareMediator mediator) : base(logger, mediator) + { + _mareHandledGameAddresses = pi.GetIpcSubscriber>("MareSynchronos.GetHandledAddresses"); + + _pluginLoaded = PluginWatcherService.GetInitialPluginState(pi, "MareSynchronos")?.IsLoaded ?? false; + + Mediator.SubscribeKeyed(this, "MareSynchronos", (msg) => + { + _pluginLoaded = msg.IsLoaded; + }); + } + + public bool APIAvailable { get; private set; } = false; + + // Must be called on framework thread + public IReadOnlyList GetHandledGameAddresses() + { + if (!_pluginLoaded) return _emptyList; + + try + { + return _mareHandledGameAddresses.InvokeFunc(); + } + catch + { + return _emptyList; + } + } +} diff --git a/MareSynchronos/Interop/Ipc/IpcCallerMoodles.cs b/MareSynchronos/Interop/Ipc/IpcCallerMoodles.cs new file mode 100644 index 0000000..44b6ce5 --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcCallerMoodles.cs @@ -0,0 +1,104 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Interop.Ipc; + +public sealed class IpcCallerMoodles : IIpcCaller +{ + private readonly ICallGateSubscriber _moodlesApiVersion; + private readonly ICallGateSubscriber _moodlesOnChange; + private readonly ICallGateSubscriber _moodlesGetStatus; + private readonly ICallGateSubscriber _moodlesSetStatus; + private readonly ICallGateSubscriber _moodlesRevertStatus; + private readonly ILogger _logger; + private readonly DalamudUtilService _dalamudUtil; + private readonly MareMediator _mareMediator; + + public IpcCallerMoodles(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, + MareMediator mareMediator) + { + _logger = logger; + _dalamudUtil = dalamudUtil; + _mareMediator = mareMediator; + + _moodlesApiVersion = pi.GetIpcSubscriber("Moodles.Version"); + _moodlesOnChange = pi.GetIpcSubscriber("Moodles.StatusManagerModified"); + _moodlesGetStatus = pi.GetIpcSubscriber("Moodles.GetStatusManagerByPtr"); + _moodlesSetStatus = pi.GetIpcSubscriber("Moodles.SetStatusManagerByPtr"); + _moodlesRevertStatus = pi.GetIpcSubscriber("Moodles.ClearStatusManagerByPtr"); + + _moodlesOnChange.Subscribe(OnMoodlesChange); + + CheckAPI(); + } + + private void OnMoodlesChange(IPlayerCharacter character) + { + _mareMediator.Publish(new MoodlesMessage(character.Address)); + } + + public bool APIAvailable { get; private set; } = false; + + public void CheckAPI() + { + try + { + APIAvailable = _moodlesApiVersion.InvokeFunc() == 1; + } + catch + { + APIAvailable = false; + } + } + + public void Dispose() + { + _moodlesOnChange.Unsubscribe(OnMoodlesChange); + } + + public async Task GetStatusAsync(nint address) + { + if (!APIAvailable) return null; + + try + { + return await _dalamudUtil.RunOnFrameworkThread(() => _moodlesGetStatus.InvokeFunc(address)).ConfigureAwait(false); + + } + catch (Exception e) + { + _logger.LogWarning(e, "Could not Get Moodles Status"); + return null; + } + } + + public async Task SetStatusAsync(nint pointer, string status) + { + if (!APIAvailable) return; + try + { + await _dalamudUtil.RunOnFrameworkThread(() => _moodlesSetStatus.InvokeAction(pointer, status)).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogWarning(e, "Could not Set Moodles Status"); + } + } + + public async Task RevertStatusAsync(nint pointer) + { + if (!APIAvailable) return; + try + { + await _dalamudUtil.RunOnFrameworkThread(() => _moodlesRevertStatus.InvokeAction(pointer)).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogWarning(e, "Could not Set Moodles Status"); + } + } +} diff --git a/MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs b/MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs new file mode 100644 index 0000000..d51cb8c --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs @@ -0,0 +1,366 @@ +using Dalamud.Plugin; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; +using System.Collections.Concurrent; + +namespace MareSynchronos.Interop.Ipc; + +public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCaller +{ + private readonly DalamudUtilService _dalamudUtil; + private readonly MareMediator _mareMediator; + 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; + _mareMediator.Publish(new PenumbraDirectoryChangedMessage(_penumbraModDirectory)); + } + } + } + + private readonly ConcurrentDictionary _penumbraRedrawRequests = new(); + + 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 bool _pluginLoaded; + private Version _pluginVersion; + + public IpcCallerPenumbra(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, + MareMediator mareMediator, RedrawManager redrawManager) : base(logger, mareMediator) + { + _dalamudUtil = dalamudUtil; + _mareMediator = mareMediator; + _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) + _mareMediator.Publish(new PenumbraModSettingChangedMessage()); + }); + _penumbraConvertTextureFile = new ConvertTextureFile(pi); + _penumbraResourcePaths = new GetGameObjectResourcePaths(pi); + + _penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded); + + var plugin = PluginWatcherService.GetInitialPluginState(pi, "Penumbra"); + + _pluginLoaded = plugin?.IsLoaded ?? false; + _pluginVersion = plugin?.Version ?? new(0, 0, 0, 0); + + Mediator.SubscribeKeyed(this, "Penumbra", (msg) => + { + _pluginLoaded = msg.IsLoaded; + _pluginVersion = msg.Version; + CheckAPI(); + }); + + 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() + { + bool penumbraAvailable = false; + try + { + penumbraAvailable = _pluginLoaded && _pluginVersion >= new Version(1, 5, 1, 0); + try + { + penumbraAvailable &= _penumbraEnabled.Invoke(); + } + catch + { + penumbraAvailable = false; + } + _shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable; + APIAvailable = penumbraAvailable; + } + catch + { + APIAvailable = penumbraAvailable; + } + finally + { + if (!penumbraAvailable && !_shownPenumbraUnavailable) + { + _shownPenumbraUnavailable = true; + _mareMediator.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 Snowcloak. If you just updated Penumbra, ignore this message.", + NotificationType.Error)); + } + } + } + + public void CheckModDirectory() + { + if (!APIAvailable) + { + ModDirectory = string.Empty; + } + else + { + ModDirectory = _penumbraResolveModDir!.Invoke().ToLowerInvariant(); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _redrawManager.Cancel(); + + _penumbraModSettingChanged.Dispose(); + _penumbraGameObjectResourcePathResolved.Dispose(); + _penumbraDispose.Dispose(); + _penumbraInit.Dispose(); + _penumbraObjectIsRedrawn.Dispose(); + } + + public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collName, int idx) + { + if (!APIAvailable) return; + + await _dalamudUtil.RunOnFrameworkThread(() => + { + var retAssign = _penumbraAssignTemporaryCollection.Invoke(collName, idx, forceAssignment: true); + logger.LogTrace("Assigning Temp Collection {collName} to index {idx}, Success: {ret}", collName, idx, retAssign); + return collName; + }).ConfigureAwait(false); + } + + public async Task ConvertTextureFiles(ILogger logger, Dictionary textures, IProgress<(string, int)> progress, CancellationToken token) + { + if (!APIAvailable) return; + + _mareMediator.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); + } + } + } + } + _mareMediator.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(() => + { + Guid collId; + var collName = "ElfSync_" + uid; + PenumbraApiEc penEC = _penumbraCreateNamedTemporaryCollection.Invoke(uid, collName, out collId); + logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId); + if (penEC != PenumbraApiEc.Success) + { + logger.LogError("Failed to create temporary collection for {collName} with error code {penEC}. Please include this line in any error reports", collName, penEC); + return Guid.Empty; + } + 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 void RedrawNow(ILogger logger, Guid applicationId, int objectIndex) + { + if (!APIAvailable || _dalamudUtil.IsZoning) return; + logger.LogTrace("[{applicationId}] Immediately redrawing object index {objId}", applicationId, objectIndex); + _penumbraRedraw.Invoke(objectIndex); + } + + 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("MareChara_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("MareChara_Files", collId, 0); + logger.LogTrace("[{applicationId}] Removing temp files mod for {collId}, Success: {ret}", applicationId, collId, retRemove); + var retAdd = _penumbraAddTemporaryMod.Invoke("MareChara_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 + { + _mareMediator.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) + { + _mareMediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2)); + } + } + + private void PenumbraDispose() + { + _redrawManager.Cancel(); + _mareMediator.Publish(new PenumbraDisposedMessage()); + } + + private void PenumbraInit() + { + APIAvailable = true; + ModDirectory = _penumbraResolveModDir.Invoke(); + _mareMediator.Publish(new PenumbraInitializedMessage()); + _penumbraRedraw!.Invoke(0, setting: RedrawType.Redraw); + } +} diff --git a/MareSynchronos/Interop/Ipc/IpcCallerPetNames.cs b/MareSynchronos/Interop/Ipc/IpcCallerPetNames.cs new file mode 100644 index 0000000..a662178 --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcCallerPetNames.cs @@ -0,0 +1,158 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Interop.Ipc; + +public sealed class IpcCallerPetNames : IIpcCaller +{ + private readonly ILogger _logger; + private readonly DalamudUtilService _dalamudUtil; + private readonly MareMediator _mareMediator; + + private readonly ICallGateSubscriber _petnamesReady; + private readonly ICallGateSubscriber _petnamesDisposing; + private readonly ICallGateSubscriber<(uint, uint)> _apiVersion; + private readonly ICallGateSubscriber _enabled; + + private readonly ICallGateSubscriber _playerDataChanged; + private readonly ICallGateSubscriber _getPlayerData; + private readonly ICallGateSubscriber _setPlayerData; + private readonly ICallGateSubscriber _clearPlayerData; + + public IpcCallerPetNames(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, + MareMediator mareMediator) + { + _logger = logger; + _dalamudUtil = dalamudUtil; + _mareMediator = mareMediator; + + _petnamesReady = pi.GetIpcSubscriber("PetRenamer.Ready"); + _petnamesDisposing = pi.GetIpcSubscriber("PetRenamer.Disposing"); + _apiVersion = pi.GetIpcSubscriber<(uint, uint)>("PetRenamer.ApiVersion"); + _enabled = pi.GetIpcSubscriber("PetRenamer.Enabled"); + + _playerDataChanged = pi.GetIpcSubscriber("PetRenamer.PlayerDataChanged"); + _getPlayerData = pi.GetIpcSubscriber("PetRenamer.GetPlayerData"); + _setPlayerData = pi.GetIpcSubscriber("PetRenamer.SetPlayerData"); + _clearPlayerData = pi.GetIpcSubscriber("PetRenamer.ClearPlayerData"); + + _petnamesReady.Subscribe(OnPetNicknamesReady); + _petnamesDisposing.Subscribe(OnPetNicknamesDispose); + _playerDataChanged.Subscribe(OnLocalPetNicknamesDataChange); + + CheckAPI(); + } + + public bool APIAvailable { get; private set; } = false; + + public void CheckAPI() + { + try + { + APIAvailable = _enabled?.InvokeFunc() ?? false; + if (APIAvailable) + { + APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 3, Item2: >= 1 }; + } + } + catch + { + APIAvailable = false; + } + } + + private void OnPetNicknamesReady() + { + CheckAPI(); + _mareMediator.Publish(new PetNamesReadyMessage()); + } + + private void OnPetNicknamesDispose() + { + _mareMediator.Publish(new PetNamesMessage(string.Empty)); + } + + public string GetLocalNames() + { + if (!APIAvailable) return string.Empty; + + try + { + string localNameData = _getPlayerData.InvokeFunc(); + return string.IsNullOrEmpty(localNameData) ? string.Empty : localNameData; + } + catch (Exception e) + { + _logger.LogWarning(e, "Could not obtain Pet Nicknames data"); + } + + return string.Empty; + } + + public async Task SetPlayerData(nint character, string playerData) + { + if (!APIAvailable) return; + + _logger.LogTrace("Applying Pet Nicknames data to {chara}", character.ToString("X")); + + try + { + await _dalamudUtil.RunOnFrameworkThread(() => + { + if (string.IsNullOrEmpty(playerData)) + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj is IPlayerCharacter pc) + { + _clearPlayerData.InvokeAction(pc.ObjectIndex); + } + } + else + { + _setPlayerData.InvokeAction(playerData); + } + }).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogWarning(e, "Could not apply Pet Nicknames data"); + } + } + + public async Task ClearPlayerData(nint characterPointer) + { + if (!APIAvailable) return; + try + { + await _dalamudUtil.RunOnFrameworkThread(() => + { + var gameObj = _dalamudUtil.CreateGameObject(characterPointer); + if (gameObj is IPlayerCharacter pc) + { + _logger.LogTrace("Pet Nicknames removing for {addr}", pc.Address.ToString("X")); + _clearPlayerData.InvokeAction(pc.ObjectIndex); + } + }).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogWarning(e, "Could not clear Pet Nicknames data"); + } + } + + private void OnLocalPetNicknamesDataChange(string data) + { + _mareMediator.Publish(new PetNamesMessage(data)); + } + + public void Dispose() + { + _petnamesReady.Unsubscribe(OnPetNicknamesReady); + _petnamesDisposing.Unsubscribe(OnPetNicknamesDispose); + _playerDataChanged.Unsubscribe(OnLocalPetNicknamesDataChange); + } +} diff --git a/MareSynchronos/Interop/Ipc/IpcManager.cs b/MareSynchronos/Interop/Ipc/IpcManager.cs new file mode 100644 index 0000000..dcb4ee1 --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcManager.cs @@ -0,0 +1,68 @@ +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Interop.Ipc; + +public sealed partial class IpcManager : DisposableMediatorSubscriberBase +{ + public IpcManager(ILogger logger, MareMediator mediator, + IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc, + IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio) : base(logger, mediator) + { + CustomizePlus = customizeIpc; + Heels = heelsIpc; + Glamourer = glamourerIpc; + Penumbra = penumbraIpc; + Honorific = honorificIpc; + Moodles = moodlesIpc; + PetNames = ipcCallerPetNames; + Brio = ipcCallerBrio; + + if (Initialized) + { + Mediator.Publish(new PenumbraInitializedMessage()); + } + + Mediator.Subscribe(this, (_) => PeriodicApiStateCheck()); + + try + { + PeriodicApiStateCheck(); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to check for some IPC, plugin not installed?"); + } + } + + public bool Initialized => Penumbra.APIAvailable && Glamourer.APIAvailable; + + public IpcCallerCustomize CustomizePlus { get; init; } + public IpcCallerHonorific Honorific { get; init; } + public IpcCallerHeels Heels { get; init; } + public IpcCallerGlamourer Glamourer { get; } + public IpcCallerPenumbra Penumbra { get; } + public IpcCallerMoodles Moodles { get; } + public IpcCallerPetNames PetNames { get; } + + public IpcCallerBrio Brio { get; } + + private int _stateCheckCounter = -1; + + private void PeriodicApiStateCheck() + { + // Stagger API checks + if (++_stateCheckCounter > 8) + _stateCheckCounter = 0; + int i = _stateCheckCounter; + if (i == 0) Penumbra.CheckAPI(); + if (i == 1) Penumbra.CheckModDirectory(); + if (i == 2) Glamourer.CheckAPI(); + if (i == 3) Heels.CheckAPI(); + if (i == 4) CustomizePlus.CheckAPI(); + if (i == 5) Honorific.CheckAPI(); + if (i == 6) Moodles.CheckAPI(); + if (i == 7) PetNames.CheckAPI(); + if (i == 8) Brio.CheckAPI(); + } +} \ No newline at end of file diff --git a/MareSynchronos/Interop/Ipc/IpcProvider.cs b/MareSynchronos/Interop/Ipc/IpcProvider.cs new file mode 100644 index 0000000..f268a2b --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcProvider.cs @@ -0,0 +1,196 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Interop.Ipc; + +public class IpcProvider : IHostedService, IMediatorSubscriber +{ + private readonly ILogger _logger; + private readonly IDalamudPluginInterface _pi; + private readonly MareConfigService _mareConfig; + private readonly CharaDataManager _charaDataManager; + private ICallGateProvider? _loadFileProvider; + private ICallGateProvider>? _loadFileAsyncProvider; + private ICallGateProvider>? _handledGameAddresses; + private readonly List _activeGameObjectHandlers = []; + + private ICallGateProvider? _loadFileProviderMare; + private ICallGateProvider>? _loadFileAsyncProviderMare; + private ICallGateProvider>? _handledGameAddressesMare; + + private bool _marePluginEnabled = false; + private bool _impersonating = false; + private DateTime _unregisterTime = DateTime.UtcNow; + private CancellationTokenSource _registerDelayCts = new(); + + public bool MarePluginEnabled => _marePluginEnabled; + public bool ImpersonationActive => _impersonating; + + public MareMediator Mediator { get; init; } + + public IpcProvider(ILogger logger, IDalamudPluginInterface pi, MareConfigService mareConfig, + CharaDataManager charaDataManager, MareMediator mareMediator) + { + _logger = logger; + _pi = pi; + _mareConfig = mareConfig; + _charaDataManager = charaDataManager; + Mediator = mareMediator; + + Mediator.Subscribe(this, (msg) => + { + if (msg.OwnedObject) return; + _activeGameObjectHandlers.Add(msg.GameObjectHandler); + }); + Mediator.Subscribe(this, (msg) => + { + if (msg.OwnedObject) return; + _activeGameObjectHandlers.Remove(msg.GameObjectHandler); + }); + + _marePluginEnabled = PluginWatcherService.GetInitialPluginState(pi, "MareSynchronos")?.IsLoaded ?? false; + Mediator.SubscribeKeyed(this, "MareSynchronos", p => { + _marePluginEnabled = p.IsLoaded; + HandleMareImpersonation(automatic: true); + }); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("Starting IpcProvider Service"); + _loadFileProvider = _pi.GetIpcProvider("ElfSync.LoadMcdf"); + _loadFileProvider.RegisterFunc(LoadMcdf); + _loadFileAsyncProvider = _pi.GetIpcProvider>("SnowcloakSync.LoadMcdfAsync"); + _loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync); + _handledGameAddresses = _pi.GetIpcProvider>("SnowcloakSync.GetHandledAddresses"); + _handledGameAddresses.RegisterFunc(GetHandledAddresses); + + _loadFileProviderMare = _pi.GetIpcProvider("MareSynchronos.LoadMcdf"); + _loadFileAsyncProviderMare = _pi.GetIpcProvider>("MareSynchronos.LoadMcdfAsync"); + _handledGameAddressesMare = _pi.GetIpcProvider>("MareSynchronos.GetHandledAddresses"); + HandleMareImpersonation(automatic: true); + + _logger.LogInformation("Started IpcProviderService"); + return Task.CompletedTask; + } + + public void HandleMareImpersonation(bool automatic = false) + { + if (_marePluginEnabled) + { + if (_impersonating) + { + _loadFileProviderMare?.UnregisterFunc(); + _loadFileAsyncProviderMare?.UnregisterFunc(); + _handledGameAddressesMare?.UnregisterFunc(); + _impersonating = false; + _unregisterTime = DateTime.UtcNow; + _logger.LogDebug("Unregistered MareSynchronos API"); + } + } + else + { + if (_mareConfig.Current.MareAPI) + { + var cancelToken = _registerDelayCts.Token; + Task.Run(async () => + { + // Wait before registering to reduce the chance of a race condition + if (automatic) + await Task.Delay(5000); + + if (cancelToken.IsCancellationRequested) + return; + + if (_marePluginEnabled) + { + _logger.LogDebug("Not registering MareSynchronos API: Mare plugin is loaded"); + return; + } + + _loadFileProviderMare?.RegisterFunc(LoadMcdf); + _loadFileAsyncProviderMare?.RegisterFunc(LoadMcdfAsync); + _handledGameAddressesMare?.RegisterFunc(GetHandledAddresses); + _impersonating = true; + _logger.LogDebug("Registered MareSynchronos API"); + }, cancelToken); + } + else + { + _registerDelayCts = _registerDelayCts.CancelRecreate(); + if (_impersonating) + { + _loadFileProviderMare?.UnregisterFunc(); + _loadFileAsyncProviderMare?.UnregisterFunc(); + _handledGameAddressesMare?.UnregisterFunc(); + _impersonating = false; + _unregisterTime = DateTime.UtcNow; + _logger.LogDebug("Unregistered MareSynchronos API"); + } + } + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("Stopping IpcProvider Service"); + _loadFileProvider?.UnregisterFunc(); + _loadFileAsyncProvider?.UnregisterFunc(); + _handledGameAddresses?.UnregisterFunc(); + + _registerDelayCts.Cancel(); + if (_impersonating) + { + _loadFileProviderMare?.UnregisterFunc(); + _loadFileAsyncProviderMare?.UnregisterFunc(); + _handledGameAddressesMare?.UnregisterFunc(); + } + + Mediator.UnsubscribeAll(this); + return Task.CompletedTask; + } + + private async Task LoadMcdfAsync(string path, IGameObject target) + { + await ApplyFileAsync(path, target).ConfigureAwait(false); + + return true; + } + + private bool LoadMcdf(string path, IGameObject target) + { + _ = Task.Run(async () => await ApplyFileAsync(path, target).ConfigureAwait(false)).ConfigureAwait(false); + + return true; + } + + private async Task ApplyFileAsync(string path, IGameObject target) + { + _charaDataManager.LoadMcdf(path); + await (_charaDataManager.LoadedMcdfHeader ?? Task.CompletedTask).ConfigureAwait(false); + _charaDataManager.McdfApplyToTarget(target.Name.TextValue); + } + + private List GetHandledAddresses() + { + if (!_impersonating) + { + if ((DateTime.UtcNow - _unregisterTime).TotalSeconds >= 1.0) + { + _logger.LogWarning("GetHandledAddresses called when it should not be registered"); + _handledGameAddressesMare?.UnregisterFunc(); + } + return []; + } + + return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList(); + } +} diff --git a/MareSynchronos/Interop/Ipc/RedrawManager.cs b/MareSynchronos/Interop/Ipc/RedrawManager.cs new file mode 100644 index 0000000..e8240fc --- /dev/null +++ b/MareSynchronos/Interop/Ipc/RedrawManager.cs @@ -0,0 +1,54 @@ +using Dalamud.Game.ClientState.Objects.Types; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace MareSynchronos.Interop.Ipc; + +public class RedrawManager +{ + private readonly MareMediator _mareMediator; + private readonly DalamudUtilService _dalamudUtil; + private readonly ConcurrentDictionary _penumbraRedrawRequests = []; + private CancellationTokenSource _disposalCts = new(); + + public SemaphoreSlim RedrawSemaphore { get; init; } = new(2, 2); + + public RedrawManager(MareMediator mareMediator, DalamudUtilService dalamudUtil) + { + _mareMediator = mareMediator; + _dalamudUtil = dalamudUtil; + } + + public async Task PenumbraRedrawInternalAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, Action action, CancellationToken token) + { + _mareMediator.Publish(new PenumbraStartRedrawMessage(handler.Address)); + + _penumbraRedrawRequests[handler.Address] = true; + + try + { + using CancellationTokenSource cancelToken = new CancellationTokenSource(); + using CancellationTokenSource combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, token, _disposalCts.Token); + var combinedToken = combinedCts.Token; + cancelToken.CancelAfter(TimeSpan.FromSeconds(15)); + await handler.ActOnFrameworkAfterEnsureNoDrawAsync(action, combinedToken).ConfigureAwait(false); + + if (!_disposalCts.Token.IsCancellationRequested) + await _dalamudUtil.WaitWhileCharacterIsDrawing(logger, handler, applicationId, 30000, combinedToken).ConfigureAwait(false); + } + finally + { + _penumbraRedrawRequests[handler.Address] = false; + _mareMediator.Publish(new PenumbraEndRedrawMessage(handler.Address)); + } + } + + internal void Cancel() + { + _disposalCts = _disposalCts.CancelRecreate(); + } +} diff --git a/MareSynchronos/Interop/VfxSpawnManager.cs b/MareSynchronos/Interop/VfxSpawnManager.cs new file mode 100644 index 0000000..90820a5 --- /dev/null +++ b/MareSynchronos/Interop/VfxSpawnManager.cs @@ -0,0 +1,203 @@ +using Dalamud.Memory; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; + +namespace MareSynchronos.Interop; + +/// +/// Code for spawning mostly taken from https://git.anna.lgbt/anna/OrangeGuidanceTomestone/src/branch/main/client/Vfx.cs +/// +public unsafe class VfxSpawnManager : DisposableMediatorSubscriberBase +{ + private static readonly byte[] _pool = "Client.System.Scheduler.Instance.VfxObject\0"u8.ToArray(); + + #region signatures + #pragma warning disable CS0649 + [Signature("E8 ?? ?? ?? ?? F3 0F 10 35 ?? ?? ?? ?? 48 89 43 08")] + private readonly delegate* unmanaged _staticVfxCreate; + + [Signature("E8 ?? ?? ?? ?? ?? ?? ?? 8B 4A ?? 85 C9")] + private readonly delegate* unmanaged _staticVfxRun; + + [Signature("40 53 48 83 EC 20 48 8B D9 48 8B 89 ?? ?? ?? ?? 48 85 C9 74 28 33 D2 E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9")] + private readonly delegate* unmanaged _staticVfxRemove; + #pragma warning restore CS0649 + #endregion + + public VfxSpawnManager(ILogger logger, IGameInteropProvider gameInteropProvider, MareMediator mareMediator) + : base(logger, mareMediator) + { + gameInteropProvider.InitializeFromAttributes(this); + mareMediator.Subscribe(this, (msg) => + { + ChangeSpawnVisibility(0f); + }); + mareMediator.Subscribe(this, (msg) => + { + RestoreSpawnVisiblity(); + }); + mareMediator.Subscribe(this, (msg) => + { + ChangeSpawnVisibility(0f); + }); + mareMediator.Subscribe(this, (msg) => + { + RestoreSpawnVisiblity(); + }); + } + + private unsafe void RestoreSpawnVisiblity() + { + foreach (var vfx in _spawnedObjects) + { + ((VfxStruct*)vfx.Value.Address)->Alpha = vfx.Value.Visibility; + } + } + + private unsafe void ChangeSpawnVisibility(float visibility) + { + foreach (var vfx in _spawnedObjects) + { + ((VfxStruct*)vfx.Value.Address)->Alpha = visibility; + } + } + + private readonly Dictionary _spawnedObjects = []; + + private VfxStruct* SpawnStatic(string path, Vector3 pos, Quaternion rotation, float r, float g, float b, float a, Vector3 scale) + { + VfxStruct* vfx; + fixed (byte* terminatedPath = Encoding.UTF8.GetBytes(path).NullTerminate()) + { + fixed (byte* pool = _pool) + { + vfx = _staticVfxCreate(terminatedPath, pool); + } + } + + if (vfx == null) + { + return null; + } + + vfx->Position = new Vector3(pos.X, pos.Y + 1, pos.Z); + vfx->Rotation = new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W); + + vfx->SomeFlags &= 0xF7; + vfx->Flags |= 2; + vfx->Red = r; + vfx->Green = g; + vfx->Blue = b; + vfx->Scale = scale; + + vfx->Alpha = a; + + _staticVfxRun(vfx, 0.0f, -1); + + return vfx; + } + + public Guid? SpawnObject(Vector3 position, Quaternion rotation, Vector3 scale, float r = 1f, float g = 1f, float b = 1f, float a = 0.5f) + { + Logger.LogDebug("Trying to Spawn orb VFX at {pos}, {rot}", position, rotation); + var vfx = SpawnStatic("bgcommon/world/common/vfx_for_event/eff/b0150_eext_y.avfx", position, rotation, r, g, b, a, scale); + if (vfx == null || (nint)vfx == nint.Zero) + { + Logger.LogDebug("Failed to Spawn VFX at {pos}, {rot}", position, rotation); + return null; + } + Guid guid = Guid.NewGuid(); + Logger.LogDebug("Spawned VFX at {pos}, {rot}: 0x{ptr:X}", position, rotation, (nint)vfx); + + _spawnedObjects[guid] = ((nint)vfx, a); + + return guid; + } + + public unsafe void MoveObject(Guid id, Vector3 newPosition) + { + if (_spawnedObjects.TryGetValue(id, out var vfxValue)) + { + if (vfxValue.Address == nint.Zero) return; + var vfx = (VfxStruct*)vfxValue.Address; + vfx->Position = newPosition with { Y = newPosition.Y + 1 }; + vfx->Flags |= 2; + } + } + + public void DespawnObject(Guid? id) + { + if (id == null) return; + if (_spawnedObjects.Remove(id.Value, out var value)) + { + Logger.LogDebug("Despawning {obj:X}", value.Address); + _staticVfxRemove((VfxStruct*)value.Address); + } + } + + private void RemoveAllVfx() + { + foreach (var obj in _spawnedObjects.Values) + { + Logger.LogDebug("Despawning {obj:X}", obj); + _staticVfxRemove((VfxStruct*)obj.Address); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing) + { + RemoveAllVfx(); + } + } + + [StructLayout(LayoutKind.Explicit)] + internal struct VfxStruct + { + [FieldOffset(0x38)] + public byte Flags; + + [FieldOffset(0x50)] + public Vector3 Position; + + [FieldOffset(0x60)] + public Quaternion Rotation; + + [FieldOffset(0x70)] + public Vector3 Scale; + + [FieldOffset(0x128)] + public int ActorCaster; + + [FieldOffset(0x130)] + public int ActorTarget; + + [FieldOffset(0x1B8)] + public int StaticCaster; + + [FieldOffset(0x1C0)] + public int StaticTarget; + + [FieldOffset(0x248)] + public byte SomeFlags; + + [FieldOffset(0x260)] + public float Red; + + [FieldOffset(0x264)] + public float Green; + + [FieldOffset(0x268)] + public float Blue; + + [FieldOffset(0x26C)] + public float Alpha; + } +} diff --git a/MareSynchronos/MareConfiguration/CharaDataConfigService.cs b/MareSynchronos/MareConfiguration/CharaDataConfigService.cs new file mode 100644 index 0000000..c0f4f15 --- /dev/null +++ b/MareSynchronos/MareConfiguration/CharaDataConfigService.cs @@ -0,0 +1,11 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class CharaDataConfigService : ConfigurationServiceBase +{ + public const string ConfigName = "charadata.json"; + + public CharaDataConfigService(string configDir) : base(configDir) { } + public override string ConfigurationName => ConfigName; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ConfigurationExtensions.cs b/MareSynchronos/MareConfiguration/ConfigurationExtensions.cs new file mode 100644 index 0000000..a876578 --- /dev/null +++ b/MareSynchronos/MareConfiguration/ConfigurationExtensions.cs @@ -0,0 +1,13 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public static class ConfigurationExtensions +{ + public static bool HasValidSetup(this MareConfig configuration) + { + return configuration.AcceptedAgreement && configuration.InitialScanComplete + && !string.IsNullOrEmpty(configuration.CacheFolder) + && Directory.Exists(configuration.CacheFolder); + } +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs b/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs new file mode 100644 index 0000000..5cd9112 --- /dev/null +++ b/MareSynchronos/MareConfiguration/ConfigurationMigrator.cs @@ -0,0 +1,25 @@ +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.MareConfiguration; + +public class ConfigurationMigrator(ILogger logger) : IHostedService +{ + private readonly ILogger _logger = logger; + + public void Migrate() + { + } + + public Task StartAsync(CancellationToken cancellationToken) + { + Migrate(); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/MareSynchronos/MareConfiguration/ConfigurationSaveService.cs b/MareSynchronos/MareConfiguration/ConfigurationSaveService.cs new file mode 100644 index 0000000..64a8ea1 --- /dev/null +++ b/MareSynchronos/MareConfiguration/ConfigurationSaveService.cs @@ -0,0 +1,137 @@ +using MareSynchronos.MareConfiguration.Configurations; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Reflection; +using System.Text.Json; + +namespace MareSynchronos.MareConfiguration; + +public class ConfigurationSaveService : IHostedService +{ + private readonly HashSet _configsToSave = []; + private readonly ILogger _logger; + private readonly SemaphoreSlim _configSaveSemaphore = new(1, 1); + private readonly CancellationTokenSource _configSaveCheckCts = new(); + public const string BackupFolder = "config_backup"; + private readonly MethodInfo _saveMethod; + + public ConfigurationSaveService(ILogger logger, IEnumerable> configs) + { + foreach (var config in configs) + { + config.ConfigSave += OnConfigurationSave; + } + _logger = logger; +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + _saveMethod = GetType().GetMethod(nameof(SaveConfig), BindingFlags.Instance | BindingFlags.NonPublic)!; +#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + } + + private void OnConfigurationSave(object? sender, EventArgs e) + { + _configSaveSemaphore.Wait(); + _configsToSave.Add(sender!); + _configSaveSemaphore.Release(); + } + + private async Task PeriodicSaveCheck(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + try + { + await SaveConfigs().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during SaveConfigs"); + } + + await Task.Delay(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false); + } + } + + private async Task SaveConfigs() + { + if (_configsToSave.Count == 0) return; + + await _configSaveSemaphore.WaitAsync().ConfigureAwait(false); + var configList = _configsToSave.ToList(); + _configsToSave.Clear(); + _configSaveSemaphore.Release(); + + foreach (var config in configList) + { + var expectedType = config.GetType().BaseType!.GetGenericArguments()[0]; + var save = _saveMethod.MakeGenericMethod(expectedType); + await ((Task)save.Invoke(this, [config])!).ConfigureAwait(false); + } + } + + private async Task SaveConfig(IConfigService config) where T : IMareConfiguration + { + _logger.LogTrace("Saving {configName}", config.ConfigurationName); + var configDir = config.ConfigurationPath.Replace(config.ConfigurationName, string.Empty); + + try + { + var configBackupFolder = Path.Join(configDir, BackupFolder); + if (!Directory.Exists(configBackupFolder)) + Directory.CreateDirectory(configBackupFolder); + + var configNameSplit = config.ConfigurationName.Split("."); + var existingConfigs = Directory.EnumerateFiles( + configBackupFolder, + configNameSplit[0] + "*") + .Select(c => new FileInfo(c)) + .OrderByDescending(c => c.LastWriteTime).ToList(); + if (existingConfigs.Skip(10).Any()) + { + foreach (var oldBak in existingConfigs.Skip(10).ToList()) + { + oldBak.Delete(); + } + } + + string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]); + _logger.LogTrace("Backing up current config to {backupPath}", backupPath); + File.Copy(config.ConfigurationPath, backupPath, overwrite: true); + FileInfo fi = new(backupPath); + fi.LastWriteTimeUtc = DateTime.UtcNow; + } + catch (Exception ex) + { + // ignore if file cannot be backupped + _logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath); + } + + var temp = config.ConfigurationPath + ".tmp"; + try + { + await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(config.Current, typeof(T), new JsonSerializerOptions() + { + WriteIndented = true + })).ConfigureAwait(false); + File.Move(temp, config.ConfigurationPath, true); + config.UpdateLastWriteTime(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during config save of {config}", config.ConfigurationName); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _ = Task.Run(() => PeriodicSaveCheck(_configSaveCheckCts.Token)); + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _configSaveCheckCts.CancelAsync().ConfigureAwait(false); + _configSaveCheckCts.Dispose(); + + await SaveConfigs().ConfigureAwait(false); + } +} diff --git a/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs b/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs new file mode 100644 index 0000000..97cee3a --- /dev/null +++ b/MareSynchronos/MareConfiguration/ConfigurationServiceBase.cs @@ -0,0 +1,141 @@ +using MareSynchronos.MareConfiguration.Configurations; +using System.Text.Json; + +namespace MareSynchronos.MareConfiguration; + +public abstract class ConfigurationServiceBase : IConfigService where T : IMareConfiguration +{ + private readonly CancellationTokenSource _periodicCheckCts = new(); + private DateTime _configLastWriteTime; + private Lazy _currentConfigInternal; + private bool _disposed = false; + + public event EventHandler? ConfigSave; + + protected ConfigurationServiceBase(string configDirectory) + { + ConfigurationDirectory = configDirectory; + + _ = Task.Run(CheckForConfigUpdatesInternal, _periodicCheckCts.Token); + + _currentConfigInternal = LazyConfig(); + } + + public string ConfigurationDirectory { get; init; } + public T Current => _currentConfigInternal.Value; + public abstract string ConfigurationName { get; } + public string ConfigurationPath => Path.Combine(ConfigurationDirectory, ConfigurationName); + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public void Save() + { + ConfigSave?.Invoke(this, EventArgs.Empty); + } + + public void UpdateLastWriteTime() + { + _configLastWriteTime = GetConfigLastWriteTime(); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing || _disposed) return; + _disposed = true; + _periodicCheckCts.Cancel(); + _periodicCheckCts.Dispose(); + } + + protected T LoadConfig() + { + T? config; + if (!File.Exists(ConfigurationPath)) + { + config = AttemptToLoadBackup(); + } + else + { + try + { + config = JsonSerializer.Deserialize(File.ReadAllText(ConfigurationPath)); + } + catch + { + // config failed to load for some reason + config = AttemptToLoadBackup(); + } + } + + if (config == null || Equals(config, default(T))) + { + config = Activator.CreateInstance(); + Save(); + } + + _configLastWriteTime = GetConfigLastWriteTime(); + return config; + } + + private T? AttemptToLoadBackup() + { + var configBackupFolder = Path.Join(ConfigurationDirectory, ConfigurationSaveService.BackupFolder); + var configNameSplit = ConfigurationName.Split("."); + if (!Directory.Exists(configBackupFolder)) + return default; + + var existingBackups = Directory.EnumerateFiles(configBackupFolder, configNameSplit[0] + "*").OrderByDescending(f => new FileInfo(f).LastWriteTimeUtc); + foreach (var file in existingBackups) + { + try + { + var config = JsonSerializer.Deserialize(File.ReadAllText(file)); + if (Equals(config, default(T))) + { + File.Delete(file); + } + + File.Copy(file, ConfigurationPath, true); + return config; + } + catch + { + // couldn't load backup, might as well delete it + File.Delete(file); + } + + } + + return default; + } + + private async Task CheckForConfigUpdatesInternal() + { + while (!_periodicCheckCts.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(5), _periodicCheckCts.Token).ConfigureAwait(false); + + var lastWriteTime = GetConfigLastWriteTime(); + if (lastWriteTime != _configLastWriteTime) + { + _currentConfigInternal = LazyConfig(); + } + } + } + + private DateTime GetConfigLastWriteTime() + { + try { return new FileInfo(ConfigurationPath).LastWriteTimeUtc; } + catch { return DateTime.MinValue; } + } + + + private Lazy LazyConfig() + { + _configLastWriteTime = GetConfigLastWriteTime(); + return new Lazy(LoadConfig); + } +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/CharaDataConfig.cs b/MareSynchronos/MareConfiguration/Configurations/CharaDataConfig.cs new file mode 100644 index 0000000..e773b37 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/CharaDataConfig.cs @@ -0,0 +1,19 @@ +using MareSynchronos.MareConfiguration.Models; + +namespace MareSynchronos.MareConfiguration.Configurations; + +public class CharaDataConfig : IMareConfiguration +{ + public bool OpenMareHubOnGposeStart { get; set; } = false; + public string LastSavedCharaDataLocation { get; set; } = string.Empty; + public Dictionary FavoriteCodes { get; set; } = []; + public bool DownloadMcdDataOnConnection { get; set; } = true; + public int Version { get; set; } = 0; + public bool NearbyOwnServerOnly { get; set; } = false; + public bool NearbyIgnoreHousingLimitations { get; set; } = false; + public bool NearbyDrawWisps { get; set; } = true; + public int NearbyDistanceFilter { get; set; } = 100; + public bool NearbyShowOwnData { get; set; } = false; + public bool ShowHelpTexts { get; set; } = true; + public bool NearbyShowAlways { get; set; } = false; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/IMareConfiguration.cs b/MareSynchronos/MareConfiguration/Configurations/IMareConfiguration.cs new file mode 100644 index 0000000..f988957 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/IMareConfiguration.cs @@ -0,0 +1,6 @@ +namespace MareSynchronos.MareConfiguration.Configurations; + +public interface IMareConfiguration +{ + int Version { get; set; } +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs new file mode 100644 index 0000000..6047545 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs @@ -0,0 +1,78 @@ +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.UI; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.MareConfiguration.Configurations; + +[Serializable] +public class MareConfig : IMareConfiguration +{ + public int ExpectedTOSVersion = 2; + public int AcceptedTOSVersion { get; set; } = 0; + public bool AcceptedAgreement { get; set; } = false; + public string CacheFolder { get; set; } = string.Empty; + public bool DisableOptionalPluginWarnings { get; set; } = false; + public bool EnableDtrEntry { get; set; } = true; + public int DtrStyle { get; set; } = 0; + public bool ShowUidInDtrTooltip { get; set; } = true; + public bool PreferNoteInDtrTooltip { get; set; } = false; + public bool UseColorsInDtr { get; set; } = true; + public DtrEntry.Colors DtrColorsDefault { get; set; } = default; + public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu); + public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u); + public bool UseNameColors { get; set; } = false; + public DtrEntry.Colors NameColors { get; set; } = new(Foreground: 0x67EBF5u, Glow: 0x00303Cu); + public DtrEntry.Colors BlockedNameColors { get; set; } = new(Foreground: 0x8AADC7, Glow: 0x000080u); + public bool EnableRightClickMenus { get; set; } = true; + public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; + public string ExportFolder { get; set; } = string.Empty; + public bool FileScanPaused { get; set; } = false; + public NotificationLocation InfoNotification { get; set; } = NotificationLocation.Toast; + public bool InitialScanComplete { get; set; } = false; + public LogLevel LogLevel { get; set; } = LogLevel.Information; + public bool LogPerformance { get; set; } = false; + public bool LogEvents { get; set; } = true; + public bool HoldCombatApplication { get; set; } = false; + public double MaxLocalCacheInGiB { get; set; } = 100; + public bool OpenGposeImportOnGposeStart { get; set; } = false; + public bool OpenPopupOnAdd { get; set; } = true; + public int ParallelDownloads { get; set; } = 10; + public int DownloadSpeedLimitInBytes { get; set; } = 0; + public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps; + [Obsolete] public bool PreferNotesOverNamesForVisible { get; set; } = false; + public float ProfileDelay { get; set; } = 1.5f; + public bool ProfilePopoutRight { get; set; } = false; + public bool ProfilesAllowNsfw { get; set; } = false; + public bool ProfilesShow { get; set; } = false; + public bool ShowSyncshellUsersInVisible { get; set; } = true; + [Obsolete] public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false; + public bool ShowCharacterNames { get; set; } = true; + public bool ShowOfflineUsersSeparately { get; set; } = true; + public bool ShowSyncshellOfflineUsersSeparately { get; set; } = true; + public bool GroupUpSyncshells { get; set; } = true; + public bool SerialApplication { get; set; } = false; + public bool ShowOnlineNotifications { get; set; } = false; + public bool ShowOnlineNotificationsOnlyForIndividualPairs { get; set; } = true; + public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false; + public bool ShowTransferBars { get; set; } = true; + public bool ShowTransferWindow { get; set; } = false; + public bool ShowUploading { get; set; } = true; + public bool ShowUploadingBigText { get; set; } = true; + public bool ShowVisibleUsersSeparately { get; set; } = true; + public int TimeSpanBetweenScansInSeconds { get; set; } = 30; + public int TransferBarsHeight { get; set; } = 12; + public bool TransferBarsShowText { get; set; } = true; + public int TransferBarsWidth { get; set; } = 250; + public bool UseAlternativeFileUpload { get; set; } = false; + public bool UseCompactor { get; set; } = false; + public int Version { get; set; } = 1; + public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both; + + public bool DisableSyncshellChat { get; set; } = false; + public int ChatColor { get; set; } = 0; // 0 means "use plugin default" + public int ChatLogKind { get; set; } = 1; // XivChatType.Debug + public bool ExtraChatAPI { get; set; } = false; + public bool ExtraChatTags { get; set; } = false; + + public bool MareAPI { get; set; } = true; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/PlayerPerformanceConfig.cs b/MareSynchronos/MareConfiguration/Configurations/PlayerPerformanceConfig.cs new file mode 100644 index 0000000..6b45ac7 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/PlayerPerformanceConfig.cs @@ -0,0 +1,16 @@ +using MareSynchronos.MareConfiguration.Models; + +namespace MareSynchronos.MareConfiguration.Configurations; + +public class PlayerPerformanceConfig : IMareConfiguration +{ + public int Version { get; set; } = 1; + public bool AutoPausePlayersExceedingThresholds { get; set; } = true; + public bool NotifyAutoPauseDirectPairs { get; set; } = true; + public bool NotifyAutoPauseGroupPairs { get; set; } = true; + public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 500; + public int TrisAutoPauseThresholdThousands { get; set; } = 400; + public bool IgnoreDirectPairs { get; set; } = true; + public TextureShrinkMode TextureShrinkMode { get; set; } = TextureShrinkMode.Default; + public bool TextureShrinkDeleteOriginal { get; set; } = false; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/RemoteConfigCache.cs b/MareSynchronos/MareConfiguration/Configurations/RemoteConfigCache.cs new file mode 100644 index 0000000..5ad3f9b --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/RemoteConfigCache.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Nodes; + +namespace MareSynchronos.MareConfiguration.Configurations; + +public class RemoteConfigCache : IMareConfiguration +{ + public int Version { get; set; } = 0; + public ulong Timestamp { get; set; } = 0; + public string Origin { get; set; } = string.Empty; + public DateTimeOffset? LastModified { get; set; } = null; + public string ETag { get; set; } = string.Empty; + public JsonObject Configuration { get; set; } = new(); +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/ServerBlockConfig.cs b/MareSynchronos/MareConfiguration/Configurations/ServerBlockConfig.cs new file mode 100644 index 0000000..df2086a --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/ServerBlockConfig.cs @@ -0,0 +1,10 @@ +using MareSynchronos.MareConfiguration.Models; + +namespace MareSynchronos.MareConfiguration.Configurations; + +[Serializable] +public class ServerBlockConfig : IMareConfiguration +{ + public Dictionary ServerBlocks { get; set; } = new(StringComparer.Ordinal); + public int Version { get; set; } = 0; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/ServerConfig.cs b/MareSynchronos/MareConfiguration/Configurations/ServerConfig.cs new file mode 100644 index 0000000..faa16c1 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/ServerConfig.cs @@ -0,0 +1,17 @@ +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.WebAPI; + +namespace MareSynchronos.MareConfiguration.Configurations; + +[Serializable] +public class ServerConfig : IMareConfiguration +{ + public int CurrentServer { get; set; } = 0; + + public List ServerStorage { get; set; } = new() + { + { new ServerStorage() { ServerName = ApiController.SnowcloakServer, ServerUri = ApiController.SnowcloakServiceUri } }, + }; + + public int Version { get; set; } = 1; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/ServerTagConfig.cs b/MareSynchronos/MareConfiguration/Configurations/ServerTagConfig.cs new file mode 100644 index 0000000..c6c8500 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/ServerTagConfig.cs @@ -0,0 +1,10 @@ +using MareSynchronos.MareConfiguration.Models; + +namespace MareSynchronos.MareConfiguration.Configurations; + +[Serializable] +public class ServerTagConfig : IMareConfiguration +{ + public Dictionary ServerTagStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase); + public int Version { get; set; } = 0; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/SyncshellConfig.cs b/MareSynchronos/MareConfiguration/Configurations/SyncshellConfig.cs new file mode 100644 index 0000000..86989e0 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/SyncshellConfig.cs @@ -0,0 +1,10 @@ +using MareSynchronos.MareConfiguration.Models; + +namespace MareSynchronos.MareConfiguration.Configurations; + +[Serializable] +public class SyncshellConfig : IMareConfiguration +{ + public Dictionary ServerShellStorage { get; set; } = new(StringComparer.Ordinal); + public int Version { get; set; } = 0; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Configurations/TransientConfig.cs b/MareSynchronos/MareConfiguration/Configurations/TransientConfig.cs new file mode 100644 index 0000000..668dc2b --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/TransientConfig.cs @@ -0,0 +1,7 @@ +namespace MareSynchronos.MareConfiguration.Configurations; + +public class TransientConfig : IMareConfiguration +{ + public Dictionary> PlayerPersistentTransientCache { get; set; } = new(StringComparer.Ordinal); + public int Version { get; set; } = 0; +} diff --git a/MareSynchronos/MareConfiguration/Configurations/UidNotesConfig.cs b/MareSynchronos/MareConfiguration/Configurations/UidNotesConfig.cs new file mode 100644 index 0000000..4941f00 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/UidNotesConfig.cs @@ -0,0 +1,10 @@ +using MareSynchronos.MareConfiguration.Models; + +namespace MareSynchronos.MareConfiguration.Configurations; + +[Serializable] +public class UidNotesConfig : IMareConfiguration +{ + public Dictionary ServerNotes { get; set; } = new(StringComparer.Ordinal); + public int Version { get; set; } = 0; +} diff --git a/MareSynchronos/MareConfiguration/Configurations/XivDataStorageConfig.cs b/MareSynchronos/MareConfiguration/Configurations/XivDataStorageConfig.cs new file mode 100644 index 0000000..7237e82 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/XivDataStorageConfig.cs @@ -0,0 +1,11 @@ +using System.Collections.Concurrent; + +namespace MareSynchronos.MareConfiguration.Configurations; + +public class XivDataStorageConfig : IMareConfiguration +{ + public ConcurrentDictionary TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase); + public ConcurrentDictionary TexDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase); + public ConcurrentDictionary>> BonesDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase); + public int Version { get; set; } = 0; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/IConfigService.cs b/MareSynchronos/MareConfiguration/IConfigService.cs new file mode 100644 index 0000000..a45917a --- /dev/null +++ b/MareSynchronos/MareConfiguration/IConfigService.cs @@ -0,0 +1,12 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public interface IConfigService : IDisposable where T : IMareConfiguration +{ + T Current { get; } + string ConfigurationName { get; } + string ConfigurationPath { get; } + public event EventHandler? ConfigSave; + void UpdateLastWriteTime(); +} diff --git a/MareSynchronos/MareConfiguration/MareConfigService.cs b/MareSynchronos/MareConfiguration/MareConfigService.cs new file mode 100644 index 0000000..39a0599 --- /dev/null +++ b/MareSynchronos/MareConfiguration/MareConfigService.cs @@ -0,0 +1,14 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class MareConfigService : ConfigurationServiceBase +{ + public const string ConfigName = "config.json"; + + public MareConfigService(string configDir) : base(configDir) + { + } + + public override string ConfigurationName => ConfigName; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/Authentication.cs b/MareSynchronos/MareConfiguration/Models/Authentication.cs new file mode 100644 index 0000000..fa18fca --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/Authentication.cs @@ -0,0 +1,9 @@ +namespace MareSynchronos.MareConfiguration.Models; + +[Serializable] +public record Authentication +{ + public string CharacterName { get; set; } = string.Empty; + public uint WorldId { get; set; } = 0; + public int SecretKeyIdx { get; set; } = -1; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/CharaDataFavorite.cs b/MareSynchronos/MareConfiguration/Models/CharaDataFavorite.cs new file mode 100644 index 0000000..29a0393 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/CharaDataFavorite.cs @@ -0,0 +1,8 @@ +namespace MareSynchronos.MareConfiguration.Models; + +[Serializable] +public class CharaDataFavorite +{ + public DateTime LastDownloaded { get; set; } = DateTime.MaxValue; + public string CustomDescription { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/DownloadSpeeds.cs b/MareSynchronos/MareConfiguration/Models/DownloadSpeeds.cs new file mode 100644 index 0000000..815da1f --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/DownloadSpeeds.cs @@ -0,0 +1,8 @@ +namespace MareSynchronos.MareConfiguration.Models; + +public enum DownloadSpeeds +{ + Bps, + KBps, + MBps +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/NotificationLocation.cs b/MareSynchronos/MareConfiguration/Models/NotificationLocation.cs new file mode 100644 index 0000000..51cd2d1 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/NotificationLocation.cs @@ -0,0 +1,16 @@ +namespace MareSynchronos.MareConfiguration.Models; + +public enum NotificationLocation +{ + Nowhere, + Chat, + Toast, + Both +} + +public enum NotificationType +{ + Info, + Warning, + Error +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/Obsolete/ServerStorageV0.cs b/MareSynchronos/MareConfiguration/Models/Obsolete/ServerStorageV0.cs new file mode 100644 index 0000000..8517873 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/Obsolete/ServerStorageV0.cs @@ -0,0 +1,29 @@ +namespace MareSynchronos.MareConfiguration.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/MareSynchronos/MareConfiguration/Models/SecretKey.cs b/MareSynchronos/MareConfiguration/Models/SecretKey.cs new file mode 100644 index 0000000..04aad1d --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/SecretKey.cs @@ -0,0 +1,8 @@ +namespace MareSynchronos.MareConfiguration.Models; + +[Serializable] +public class SecretKey +{ + public string FriendlyName { get; set; } = string.Empty; + public string Key { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/ServerBlockStorage.cs b/MareSynchronos/MareConfiguration/Models/ServerBlockStorage.cs new file mode 100644 index 0000000..642b9c2 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/ServerBlockStorage.cs @@ -0,0 +1,8 @@ +namespace MareSynchronos.MareConfiguration.Models; + +[Serializable] +public class ServerBlockStorage +{ + public List Whitelist { get; set; } = new(); + public List Blacklist { get; set; } = new(); +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/ServerNotesStorage.cs b/MareSynchronos/MareConfiguration/Models/ServerNotesStorage.cs new file mode 100644 index 0000000..75ea221 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/ServerNotesStorage.cs @@ -0,0 +1,9 @@ +namespace MareSynchronos.MareConfiguration.Models; + +[Serializable] +public class ServerNotesStorage +{ + public Dictionary GidServerComments { get; set; } = new(StringComparer.Ordinal); + public Dictionary UidServerComments { get; set; } = new(StringComparer.Ordinal); + public Dictionary UidLastSeenNames { get; set; } = new(StringComparer.Ordinal); +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/ServerShellStorage.cs b/MareSynchronos/MareConfiguration/Models/ServerShellStorage.cs new file mode 100644 index 0000000..2f9fa2a --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/ServerShellStorage.cs @@ -0,0 +1,7 @@ +namespace MareSynchronos.MareConfiguration.Models; + +[Serializable] +public class ServerShellStorage +{ + public Dictionary GidShellConfig { get; set; } = new(StringComparer.Ordinal); +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/ServerStorage.cs b/MareSynchronos/MareConfiguration/Models/ServerStorage.cs new file mode 100644 index 0000000..03d8a25 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/ServerStorage.cs @@ -0,0 +1,11 @@ +namespace MareSynchronos.MareConfiguration.Models; + +[Serializable] +public class ServerStorage +{ + public List Authentications { get; set; } = []; + public bool FullPause { get; set; } = false; + public Dictionary SecretKeys { get; set; } = []; + public string ServerName { get; set; } = string.Empty; + public string ServerUri { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/ServerTagStorage.cs b/MareSynchronos/MareConfiguration/Models/ServerTagStorage.cs new file mode 100644 index 0000000..d7f7e7d --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/ServerTagStorage.cs @@ -0,0 +1,9 @@ +namespace MareSynchronos.MareConfiguration.Models; + +[Serializable] +public class ServerTagStorage +{ + public HashSet OpenPairTags { get; set; } = new(StringComparer.Ordinal); + public HashSet ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal); + public Dictionary> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal); +} diff --git a/MareSynchronos/MareConfiguration/Models/ShellConfig.cs b/MareSynchronos/MareConfiguration/Models/ShellConfig.cs new file mode 100644 index 0000000..54eb1e1 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/ShellConfig.cs @@ -0,0 +1,10 @@ +namespace MareSynchronos.MareConfiguration.Models; + +[Serializable] +public class ShellConfig +{ + public bool Enabled { get; set; } = true; + public int ShellNumber { get; set; } + public int Color { get; set; } = 0; // 0 means "default to the global setting" + public int LogKind { get; set; } = 0; // 0 means "default to the global setting" +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/Models/TextureShrinkMode.cs b/MareSynchronos/MareConfiguration/Models/TextureShrinkMode.cs new file mode 100644 index 0000000..adbe6d0 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Models/TextureShrinkMode.cs @@ -0,0 +1,10 @@ +namespace MareSynchronos.MareConfiguration.Models; + +public enum TextureShrinkMode +{ + Never, + Default, + DefaultHiRes, + Always, + AlwaysHiRes +} diff --git a/MareSynchronos/MareConfiguration/NotesConfigService.cs b/MareSynchronos/MareConfiguration/NotesConfigService.cs new file mode 100644 index 0000000..bf8c00b --- /dev/null +++ b/MareSynchronos/MareConfiguration/NotesConfigService.cs @@ -0,0 +1,14 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class NotesConfigService : ConfigurationServiceBase +{ + public const string ConfigName = "notes.json"; + + public NotesConfigService(string configDir) : base(configDir) + { + } + + public override string ConfigurationName => ConfigName; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/PlayerPerformanceConfigService.cs b/MareSynchronos/MareConfiguration/PlayerPerformanceConfigService.cs new file mode 100644 index 0000000..6140760 --- /dev/null +++ b/MareSynchronos/MareConfiguration/PlayerPerformanceConfigService.cs @@ -0,0 +1,11 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class PlayerPerformanceConfigService : ConfigurationServiceBase +{ + public const string ConfigName = "playerperformance.json"; + public PlayerPerformanceConfigService(string configDir) : base(configDir) { } + + public override string ConfigurationName => ConfigName; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/RemoteConfigCacheService.cs b/MareSynchronos/MareConfiguration/RemoteConfigCacheService.cs new file mode 100644 index 0000000..66c7ff4 --- /dev/null +++ b/MareSynchronos/MareConfiguration/RemoteConfigCacheService.cs @@ -0,0 +1,11 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class RemoteConfigCacheService : ConfigurationServiceBase +{ + public const string ConfigName = "remotecache.json"; + + public RemoteConfigCacheService(string configDir) : base(configDir) { } + public override string ConfigurationName => ConfigName; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ServerBlockConfigService.cs b/MareSynchronos/MareConfiguration/ServerBlockConfigService.cs new file mode 100644 index 0000000..5c85f5d --- /dev/null +++ b/MareSynchronos/MareConfiguration/ServerBlockConfigService.cs @@ -0,0 +1,14 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class ServerBlockConfigService : ConfigurationServiceBase +{ + public const string ConfigName = "blocks.json"; + + public ServerBlockConfigService(string configDir) : base(configDir) + { + } + + public override string ConfigurationName => ConfigName; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ServerConfigService.cs b/MareSynchronos/MareConfiguration/ServerConfigService.cs new file mode 100644 index 0000000..185e2fe --- /dev/null +++ b/MareSynchronos/MareConfiguration/ServerConfigService.cs @@ -0,0 +1,14 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class ServerConfigService : ConfigurationServiceBase +{ + public const string ConfigName = "server.json"; + + public ServerConfigService(string configDir) : base(configDir) + { + } + + public override string ConfigurationName => ConfigName; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/ServerTagConfigService.cs b/MareSynchronos/MareConfiguration/ServerTagConfigService.cs new file mode 100644 index 0000000..fc78403 --- /dev/null +++ b/MareSynchronos/MareConfiguration/ServerTagConfigService.cs @@ -0,0 +1,14 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class ServerTagConfigService : ConfigurationServiceBase +{ + public const string ConfigName = "servertags.json"; + + public ServerTagConfigService(string configDir) : base(configDir) + { + } + + public override string ConfigurationName => ConfigName; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/SyncshellConfigService.cs b/MareSynchronos/MareConfiguration/SyncshellConfigService.cs new file mode 100644 index 0000000..4d34e5a --- /dev/null +++ b/MareSynchronos/MareConfiguration/SyncshellConfigService.cs @@ -0,0 +1,14 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class SyncshellConfigService : ConfigurationServiceBase +{ + public const string ConfigName = "syncshells.json"; + + public SyncshellConfigService(string configDir) : base(configDir) + { + } + + public override string ConfigurationName => ConfigName; +} \ No newline at end of file diff --git a/MareSynchronos/MareConfiguration/TransientConfigService.cs b/MareSynchronos/MareConfiguration/TransientConfigService.cs new file mode 100644 index 0000000..cae9d02 --- /dev/null +++ b/MareSynchronos/MareConfiguration/TransientConfigService.cs @@ -0,0 +1,14 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class TransientConfigService : ConfigurationServiceBase +{ + public const string ConfigName = "transient.json"; + + public TransientConfigService(string configDir) : base(configDir) + { + } + + public override string ConfigurationName => ConfigName; +} diff --git a/MareSynchronos/MareConfiguration/XivDataStorageService.cs b/MareSynchronos/MareConfiguration/XivDataStorageService.cs new file mode 100644 index 0000000..777f728 --- /dev/null +++ b/MareSynchronos/MareConfiguration/XivDataStorageService.cs @@ -0,0 +1,12 @@ +using MareSynchronos.MareConfiguration.Configurations; + +namespace MareSynchronos.MareConfiguration; + +public class XivDataStorageService : ConfigurationServiceBase +{ + public const string ConfigName = "xivdatastorage.json"; + + public XivDataStorageService(string configDir) : base(configDir) { } + + public override string ConfigurationName => ConfigName; +} diff --git a/MareSynchronos/MarePlugin.cs b/MareSynchronos/MarePlugin.cs new file mode 100644 index 0000000..6f8d5c5 --- /dev/null +++ b/MareSynchronos/MarePlugin.cs @@ -0,0 +1,170 @@ +using MareSynchronos.FileCache; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.PlayerData.Services; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Reflection; + +namespace MareSynchronos; + +#pragma warning disable S125 // Sections of code should not be commented out +/* + (..,,...,,,,,+/, ,,.....,,+ + ..,,+++/((###%%%&&%%#(+,,.,,,+++,,,,//,,#&@@@@%+. + ...+//////////(/,,,,++,.,(###((//////////,.. .,#@@%/./ + ,..+/////////+///,.,. ,&@@@@,,/////////////+,.. ,(##+,. + ,,.+//////////++++++.. ./#%#,+/////////////+,....,/((,.., + +..////////////+++++++... .../##(,,////////////////++,,,+/(((+, + +,.+//////////////+++++++,.,,,/(((+.,////////////////////////((((#/,, + /+.+//////////++++/++++++++++,,...,++///////////////////////////((((##, + /,.////////+++++++++++++++++++++////////+++//////++/+++++//////////((((#(+, + /+.+////////+++++++++++++++++++++++++++++++++++++++++++++++++++++/////((((##+ + +,.///////////////+++++++++++++++++++++++++++++++++++++++++++++++++++///((((%/ + /.,/////////////////+++++++++++++++++++++++++++++++++++++++++++++++++++///+/(#+ + +,./////////////////+++++++++++++++++++++++++++++++++++++++++++++++,,+++++///((, + ...////////++/++++++++++++++++++++++++,,++++++++++++++++++++++++++++++++++++//(,, + ..//+,+///++++++++++++++++++,,,,+++,,,,,,,,,,,,++++++++,,+++++++++++++++++++//,,+ + ..,++,.++++++++++++++++++++++,,,,,,,,,,,,,,,,,,,++++++++,,,,,,,,,,++++++++++... + ..+++,.+++++++++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,++,..,. + ..,++++,,+++++++++++,+,,,,,,,,,,..,+++++++++,,,,,,.....................,//+,+ + ....,+++++,.,+++++++++++,,,,,,,,.+///(((((((((((((///////////////////////(((+,,, + .....,++++++++++..,+++++++++++,,.,,,.////////(((((((((((((((////////////////////+,,/ + .....,++++++++++++,..,,+++++++++,,.,../////////////////((((((((((//////////////////,,+ + ...,,+++++++++++++,.,,.,,,+++++++++,.,/////////////////(((//++++++++++++++//+++++++++/,, + ....,++++++++++++++,.,++.,++++++++++++.,+////////////////////+++++++++++++++++++++++++///,,.. + ...,++++++++++++++++..+++..+++++++++++++.,//////////////////////////++++++++++++///////++++...... + ...++++++++++++++++++..++++.,++,++++++++++.+///////////////////////////////////////////++++++..,,,.. + ...+++++++++++++++++++..+++++..,+,,+++++++++.+//////////////////////////////////////////+++++++...,,,,.. + ..++++++++++++++++++++..++++++..,+,,+++++++++.+//////////////////////////////////////++++++++++,....,,,,.. + ...+++//(//////+++++++++..++++++,.,+++++++++++++,..,....,,,+++///////////////////////++++++++++++..,,,,,,,,... + ..,++/(((((//////+++++++,.,++++++,,.,,,+++++++++++++++++++++++,.++////////////////////+++++++++++.....,,,,,,,... + ..,//#(((((///////+++++++..++++++++++,...,++,++++++++++++++++,...+++/////////////////////+,,,+++... ....,,,,,,... + ...+//(((((//////////++++++..+++++++++++++++,......,,,,++++++,,,..+++////////////////////////+,.... ...,,,,,,,... + ..,//((((////////////++++++..++++++/+++++++++++++,,...,,........,+/+//////////////////////((((/+,.. ....,.,,,,.. + ...+/////////////////////+++..++++++/+///+++++++++++++++++++++///+/+////////////////////////(((((/+... .......,,... + ..++////+++//////////////++++.+++++++++///////++++++++////////////////////////////////////+++/(((((/+.. .....,,... + .,++++++++///////////////++++..++++//////////////////////////////////////////////////////++++++/((((++.. ........ + .+++++++++////////////////++++,.+++/////////////////////////////////////////////////////+++++++++/((/++.. + .,++++++++//////////////////++++,.+++//////////////////////////////////////////////////+++++++++++++//+++.. + .++++++++//////////////////////+/,.,+++////((((////////////////////////////////////////++++++++++++++++++... + .++++++++///////////////////////+++..++++//((((((((///////////////////////////////////++++++++++++++++++++ . + .++++++///////////////////////////++,.,+++++/(((((((((/////////////////////////////+++++++++++++++++++++++,.. + .++++++////////////////////////////+++,.,+++++++/((((((((//////////////////////////++++++++++++++++++++++++.. + .+++++++///////////////////++////////++++,.,+++++++++///////////+////////////////+++++++++++++++++++++++++,.. + ..++++++++++//////////////////////+++++++..+...,+++++++++++++++/++++++++++++++++++++++++++++++++++++++++++,... + ..++++++++++++///////////////+++++++,...,,,,,.,....,,,,+++++++++++++++++++++++++++++++++++++++++++++++,,,,... + ...++++++++++++++++++++++++++,,,,...,,,,,,,,,..,,++,,,.,,,,,,,,,,,,,,,,,,+++++++++++++++++++++++++,,,,,,,,.. + ...+++++++++++++++,,,,,,,,....,,,,,,,,,,,,,,,..,,++++++,,,,,,,,,,,,,,,,+++++++++++++++++++++++++,,,,,,,,,.. + ...++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,...,++++++++++++++++++++++++++++++++++++++++++++,,,,,,,,,,... + ,....,++++++++++++++,,,+++++++,,,,,,,,,,,,,,,,,.,++++++++++++++++++++++++++++++++++++++++++++,,,,,,,,.. + +*/ +#pragma warning restore S125 // Sections of code should not be commented out + +public class MarePlugin : MediatorSubscriberBase, IHostedService +{ + private readonly DalamudUtilService _dalamudUtil; + private readonly MareConfigService _mareConfigService; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly IServiceScopeFactory _serviceScopeFactory; + private IServiceScope? _runtimeServiceScope; + private Task? _launchTask = null; + + public MarePlugin(ILogger logger, MareConfigService mareConfigService, + ServerConfigurationManager serverConfigurationManager, + DalamudUtilService dalamudUtil, + IServiceScopeFactory serviceScopeFactory, MareMediator mediator) : base(logger, mediator) + { + _mareConfigService = mareConfigService; + _serverConfigurationManager = serverConfigurationManager; + _dalamudUtil = dalamudUtil; + _serviceScopeFactory = serviceScopeFactory; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + var version = Assembly.GetExecutingAssembly().GetName().Version!; + Logger.LogInformation("Launching {name} {major}.{minor}.{build}.{rev}", "Snowcloak Sync", version.Major, version.Minor, version.Build, version.Revision); + Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(MarePlugin), Services.Events.EventSeverity.Informational, + $"Starting Snowcloak Sync {version.Major}.{version.Minor}.{version.Build}.{version.Revision}"))); + + Mediator.Subscribe(this, (msg) => { if (_launchTask == null || _launchTask.IsCompleted) _launchTask = Task.Run(WaitForPlayerAndLaunchCharacterManager); }); + Mediator.Subscribe(this, (_) => DalamudUtilOnLogIn()); + Mediator.Subscribe(this, (_) => DalamudUtilOnLogOut()); + + Mediator.StartQueueProcessing(); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + UnsubscribeAll(); + + DalamudUtilOnLogOut(); + + Logger.LogDebug("Halting MarePlugin"); + + return Task.CompletedTask; + } + + private void DalamudUtilOnLogIn() + { + Logger?.LogDebug("Client login"); + if (_launchTask == null || _launchTask.IsCompleted) _launchTask = Task.Run(WaitForPlayerAndLaunchCharacterManager); + } + + private void DalamudUtilOnLogOut() + { + Logger?.LogDebug("Client logout"); + + _runtimeServiceScope?.Dispose(); + } + + private async Task WaitForPlayerAndLaunchCharacterManager() + { + while (!await _dalamudUtil.GetIsPlayerPresentAsync().ConfigureAwait(false)) + { + await Task.Delay(100).ConfigureAwait(false); + } + + try + { + Logger?.LogDebug("Launching Managers"); + + _runtimeServiceScope?.Dispose(); + _runtimeServiceScope = _serviceScopeFactory.CreateScope(); + _runtimeServiceScope.ServiceProvider.GetRequiredService(); + _runtimeServiceScope.ServiceProvider.GetRequiredService(); + if (!_mareConfigService.Current.HasValidSetup() || !_serverConfigurationManager.HasValidConfig()) + { + Mediator.Publish(new SwitchToIntroUiMessage()); + return; + } + _runtimeServiceScope.ServiceProvider.GetRequiredService(); + _runtimeServiceScope.ServiceProvider.GetRequiredService(); + _runtimeServiceScope.ServiceProvider.GetRequiredService(); + _runtimeServiceScope.ServiceProvider.GetRequiredService(); + _runtimeServiceScope.ServiceProvider.GetRequiredService(); + _runtimeServiceScope.ServiceProvider.GetRequiredService(); + +#if !DEBUG + if (_mareConfigService.Current.LogLevel != LogLevel.Information) + { + Mediator.Publish(new NotificationMessage("Abnormal Log Level", + $"Your log level is set to '{_mareConfigService.Current.LogLevel}' which is not recommended for normal usage. Set it to '{LogLevel.Information}' in \"Snowcloak Settings -> Debug\" unless instructed otherwise.", + MareConfiguration.Models.NotificationType.Error, TimeSpan.FromSeconds(15000))); + } +#endif + } + catch (Exception ex) + { + Logger?.LogCritical(ex, "Error during launch of managers"); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj new file mode 100644 index 0000000..8ee749d --- /dev/null +++ b/MareSynchronos/MareSynchronos.csproj @@ -0,0 +1,63 @@ + + + + Snowcloak + 0.2.0.2 + https://github.com/Eauldane/SnowcloakClient/ + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + build$([System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss:fffZ")) + enable + + + + + + + + + + + diff --git a/MareSynchronos/PlayerData/Data/CharacterData.cs b/MareSynchronos/PlayerData/Data/CharacterData.cs new file mode 100644 index 0000000..f55bab8 --- /dev/null +++ b/MareSynchronos/PlayerData/Data/CharacterData.cs @@ -0,0 +1,50 @@ +using MareSynchronos.API.Data; + +using MareSynchronos.API.Data.Enum; + +namespace MareSynchronos.PlayerData.Data; + +public class CharacterData +{ + public Dictionary CustomizePlusScale { get; set; } = []; + public Dictionary> FileReplacements { get; set; } = []; + public Dictionary GlamourerString { get; set; } = []; + public string HeelsData { get; set; } = string.Empty; + public string HonorificData { get; set; } = string.Empty; + public string ManipulationString { get; set; } = string.Empty; + public string PetNamesData { get; set; } = string.Empty; + public string MoodlesData { get; set; } = string.Empty; + + public API.Data.CharacterData ToAPI() + { + Dictionary> fileReplacements = + FileReplacements.ToDictionary(k => k.Key, k => k.Value.Where(f => f.HasFileReplacement && !f.IsFileSwap) + .GroupBy(f => f.Hash, StringComparer.OrdinalIgnoreCase) + .Select(g => + { + return new FileReplacementData() + { + GamePaths = g.SelectMany(f => f.GamePaths).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(), + Hash = g.First().Hash, + }; + }).ToList()); + + foreach (var item in FileReplacements) + { + var fileSwapsToAdd = item.Value.Where(f => f.IsFileSwap).Select(f => f.ToFileReplacementDto()); + fileReplacements[item.Key].AddRange(fileSwapsToAdd); + } + + return new API.Data.CharacterData() + { + FileReplacements = fileReplacements, + GlamourerData = GlamourerString.ToDictionary(d => d.Key, d => d.Value), + ManipulationData = ManipulationString, + HeelsData = HeelsData, + CustomizePlusData = CustomizePlusScale.ToDictionary(d => d.Key, d => d.Value), + HonorificData = HonorificData, + PetNamesData = PetNamesData, + MoodlesData = MoodlesData + }; + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Data/FileReplacement.cs b/MareSynchronos/PlayerData/Data/FileReplacement.cs new file mode 100644 index 0000000..2d6e358 --- /dev/null +++ b/MareSynchronos/PlayerData/Data/FileReplacement.cs @@ -0,0 +1,42 @@ +using MareSynchronos.API.Data; + +using System.Text.RegularExpressions; + +namespace MareSynchronos.PlayerData.Data; + +public partial class FileReplacement +{ + public FileReplacement(string[] gamePaths, string filePath) + { + GamePaths = gamePaths.Select(g => g.Replace('\\', '/').ToLowerInvariant()).ToHashSet(StringComparer.Ordinal); + ResolvedPath = filePath.Replace('\\', '/'); + } + + public HashSet GamePaths { get; init; } + + public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => !string.Equals(p, ResolvedPath, StringComparison.Ordinal)); + + public string Hash { get; set; } = string.Empty; + public bool IsFileSwap => !LocalPathRegex().IsMatch(ResolvedPath) && GamePaths.All(p => !LocalPathRegex().IsMatch(p)); + public string ResolvedPath { get; init; } + + public FileReplacementData ToFileReplacementDto() + { + return new FileReplacementData + { + GamePaths = [.. GamePaths], + Hash = Hash, + FileSwapPath = IsFileSwap ? ResolvedPath : string.Empty, + }; + } + + public override string ToString() + { + return $"HasReplacement:{HasFileReplacement},IsFileSwap:{IsFileSwap} - {string.Join(",", GamePaths)} => {ResolvedPath}"; + } + +#pragma warning disable MA0009 + [GeneratedRegex(@"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript)] + private static partial Regex LocalPathRegex(); +#pragma warning restore MA0009 +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Data/FileReplacementComparer.cs b/MareSynchronos/PlayerData/Data/FileReplacementComparer.cs new file mode 100644 index 0000000..79b6bf0 --- /dev/null +++ b/MareSynchronos/PlayerData/Data/FileReplacementComparer.cs @@ -0,0 +1,47 @@ +namespace MareSynchronos.PlayerData.Data; + +public class FileReplacementComparer : IEqualityComparer +{ + private static readonly FileReplacementComparer _instance = new(); + + private FileReplacementComparer() + { } + + public static FileReplacementComparer Instance => _instance; + + public bool Equals(FileReplacement? x, FileReplacement? y) + { + if (x == null || y == null) return false; + return x.ResolvedPath.Equals(y.ResolvedPath) && CompareLists(x.GamePaths, y.GamePaths); + } + + public int GetHashCode(FileReplacement obj) + { + return HashCode.Combine(obj.ResolvedPath.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths)); + } + + private static bool CompareLists(HashSet list1, HashSet list2) + { + 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; + } + + private static int GetOrderIndependentHashCode(IEnumerable source) where T : notnull + { + int hash = 0; + foreach (T element in source) + { + hash = unchecked(hash + + EqualityComparer.Default.GetHashCode(element)); + } + return hash; + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Data/FileReplacementDataComparer.cs b/MareSynchronos/PlayerData/Data/FileReplacementDataComparer.cs new file mode 100644 index 0000000..dda146f --- /dev/null +++ b/MareSynchronos/PlayerData/Data/FileReplacementDataComparer.cs @@ -0,0 +1,49 @@ +using MareSynchronos.API.Data; + +namespace MareSynchronos.PlayerData.Data; + +public class FileReplacementDataComparer : IEqualityComparer +{ + private static readonly FileReplacementDataComparer _instance = new(); + + private FileReplacementDataComparer() + { } + + public static FileReplacementDataComparer Instance => _instance; + + public bool Equals(FileReplacementData? x, FileReplacementData? y) + { + if (x == null || y == null) return false; + return x.Hash.Equals(y.Hash) && CompareHashSets(x.GamePaths.ToHashSet(StringComparer.Ordinal), y.GamePaths.ToHashSet(StringComparer.Ordinal)) && string.Equals(x.FileSwapPath, y.FileSwapPath, StringComparison.Ordinal); + } + + public int GetHashCode(FileReplacementData obj) + { + return HashCode.Combine(obj.Hash.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths), StringComparer.Ordinal.GetHashCode(obj.FileSwapPath)); + } + + private static bool CompareHashSets(HashSet list1, HashSet list2) + { + 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; + } + + private static int GetOrderIndependentHashCode(IEnumerable source) where T : notnull + { + int hash = 0; + foreach (T element in source) + { + hash = unchecked(hash + + EqualityComparer.Default.GetHashCode(element)); + } + return hash; + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Data/PlayerChanges.cs b/MareSynchronos/PlayerData/Data/PlayerChanges.cs new file mode 100644 index 0000000..e1d6358 --- /dev/null +++ b/MareSynchronos/PlayerData/Data/PlayerChanges.cs @@ -0,0 +1,14 @@ +namespace MareSynchronos.PlayerData.Pairs; + +public enum PlayerChanges +{ + ModFiles = 1, + ModManip = 2, + Glamourer = 3, + Customize = 4, + Heels = 5, + Honorific = 7, + ForcedRedraw = 8, + Moodles = 9, + PetNames = 10, +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/FileDownloadManagerFactory.cs b/MareSynchronos/PlayerData/Factories/FileDownloadManagerFactory.cs new file mode 100644 index 0000000..f208ee9 --- /dev/null +++ b/MareSynchronos/PlayerData/Factories/FileDownloadManagerFactory.cs @@ -0,0 +1,30 @@ +using MareSynchronos.FileCache; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI.Files; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.PlayerData.Factories; + +public class FileDownloadManagerFactory +{ + private readonly FileCacheManager _fileCacheManager; + private readonly FileCompactor _fileCompactor; + private readonly FileTransferOrchestrator _fileTransferOrchestrator; + private readonly ILoggerFactory _loggerFactory; + private readonly MareMediator _mareMediator; + + public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator, + FileCacheManager fileCacheManager, FileCompactor fileCompactor) + { + _loggerFactory = loggerFactory; + _mareMediator = mareMediator; + _fileTransferOrchestrator = fileTransferOrchestrator; + _fileCacheManager = fileCacheManager; + _fileCompactor = fileCompactor; + } + + public FileDownloadManager Create() + { + return new FileDownloadManager(_loggerFactory.CreateLogger(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor); + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/GameObjectHandlerFactory.cs b/MareSynchronos/PlayerData/Factories/GameObjectHandlerFactory.cs new file mode 100644 index 0000000..c1ec506 --- /dev/null +++ b/MareSynchronos/PlayerData/Factories/GameObjectHandlerFactory.cs @@ -0,0 +1,30 @@ +using MareSynchronos.API.Data.Enum; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.PlayerData.Factories; + +public class GameObjectHandlerFactory +{ + private readonly DalamudUtilService _dalamudUtilService; + private readonly ILoggerFactory _loggerFactory; + private readonly MareMediator _mareMediator; + private readonly PerformanceCollectorService _performanceCollectorService; + + public GameObjectHandlerFactory(ILoggerFactory loggerFactory, PerformanceCollectorService performanceCollectorService, MareMediator mareMediator, + DalamudUtilService dalamudUtilService) + { + _loggerFactory = loggerFactory; + _performanceCollectorService = performanceCollectorService; + _mareMediator = mareMediator; + _dalamudUtilService = dalamudUtilService; + } + + public async Task Create(ObjectKind objectKind, Func getAddressFunc, bool isWatched = false) + { + return await _dalamudUtilService.RunOnFrameworkThread(() => new GameObjectHandler(_loggerFactory.CreateLogger(), + _performanceCollectorService, _mareMediator, _dalamudUtilService, objectKind, getAddressFunc, isWatched)).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/PairAnalyzerFactory.cs b/MareSynchronos/PlayerData/Factories/PairAnalyzerFactory.cs new file mode 100644 index 0000000..42b9cfa --- /dev/null +++ b/MareSynchronos/PlayerData/Factories/PairAnalyzerFactory.cs @@ -0,0 +1,30 @@ +using MareSynchronos.FileCache; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.PlayerData.Factories; + +public class PairAnalyzerFactory +{ + private readonly ILoggerFactory _loggerFactory; + private readonly MareMediator _mareMediator; + private readonly FileCacheManager _fileCacheManager; + private readonly XivDataAnalyzer _modelAnalyzer; + + public PairAnalyzerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, + FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer) + { + _loggerFactory = loggerFactory; + _fileCacheManager = fileCacheManager; + _mareMediator = mareMediator; + _modelAnalyzer = modelAnalyzer; + } + + public PairAnalyzer Create(Pair pair) + { + return new PairAnalyzer(_loggerFactory.CreateLogger(), pair, _mareMediator, + _fileCacheManager, _modelAnalyzer); + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/PairFactory.cs b/MareSynchronos/PlayerData/Factories/PairFactory.cs new file mode 100644 index 0000000..ad56370 --- /dev/null +++ b/MareSynchronos/PlayerData/Factories/PairFactory.cs @@ -0,0 +1,33 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.PlayerData.Factories; + +public class PairFactory +{ + private readonly PairHandlerFactory _cachedPlayerFactory; + private readonly ILoggerFactory _loggerFactory; + private readonly MareMediator _mareMediator; + private readonly MareConfigService _mareConfig; + private readonly ServerConfigurationManager _serverConfigurationManager; + + public PairFactory(ILoggerFactory loggerFactory, PairHandlerFactory cachedPlayerFactory, + MareMediator mareMediator, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) + { + _loggerFactory = loggerFactory; + _cachedPlayerFactory = cachedPlayerFactory; + _mareMediator = mareMediator; + _mareConfig = mareConfig; + _serverConfigurationManager = serverConfigurationManager; + } + + public Pair Create(UserData userData) + { + return new Pair(_loggerFactory.CreateLogger(), userData, _cachedPlayerFactory, _mareMediator, _mareConfig, _serverConfigurationManager); + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs new file mode 100644 index 0000000..0fadae8 --- /dev/null +++ b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs @@ -0,0 +1,60 @@ +using MareSynchronos.FileCache; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.PlayerData.Factories; + +public class PairHandlerFactory +{ + private readonly MareConfigService _configService; + 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 MareMediator _mareMediator; + private readonly PlayerPerformanceService _playerPerformanceService; + private readonly ServerConfigurationManager _serverConfigManager; + private readonly PluginWarningNotificationService _pluginWarningNotificationManager; + private readonly PairAnalyzerFactory _pairAnalyzerFactory; + private readonly VisibilityService _visibilityService; + + public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager, + FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService, + PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime, + FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService, + ServerConfigurationManager serverConfigManager, PairAnalyzerFactory pairAnalyzerFactory, + MareConfigService configService, VisibilityService visibilityService) + { + _loggerFactory = loggerFactory; + _gameObjectHandlerFactory = gameObjectHandlerFactory; + _ipcManager = ipcManager; + _fileDownloadManagerFactory = fileDownloadManagerFactory; + _dalamudUtilService = dalamudUtilService; + _pluginWarningNotificationManager = pluginWarningNotificationManager; + _hostApplicationLifetime = hostApplicationLifetime; + _fileCacheManager = fileCacheManager; + _mareMediator = mareMediator; + _playerPerformanceService = playerPerformanceService; + _serverConfigManager = serverConfigManager; + _pairAnalyzerFactory = pairAnalyzerFactory; + _configService = configService; + _visibilityService = visibilityService; + } + + public PairHandler Create(Pair pair) + { + return new PairHandler(_loggerFactory.CreateLogger(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory, + _ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime, + _fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager, _configService, _visibilityService); + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs new file mode 100644 index 0000000..6d8b22b --- /dev/null +++ b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs @@ -0,0 +1,365 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.FileCache; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.PlayerData.Data; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; +using CharacterData = MareSynchronos.PlayerData.Data.CharacterData; + +namespace MareSynchronos.PlayerData.Factories; + +public class PlayerDataFactory +{ + private static readonly string[] _allowedExtensionsForGamePaths = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk"]; + private readonly DalamudUtilService _dalamudUtil; + private readonly FileCacheManager _fileCacheManager; + private readonly IpcManager _ipcManager; + private readonly ILogger _logger; + private readonly PerformanceCollectorService _performanceCollector; + private readonly XivDataAnalyzer _modelAnalyzer; + private readonly MareMediator _mareMediator; + private readonly TransientResourceManager _transientResourceManager; + + public PlayerDataFactory(ILogger logger, DalamudUtilService dalamudUtil, IpcManager ipcManager, + TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory, + PerformanceCollectorService performanceCollector, XivDataAnalyzer modelAnalyzer, MareMediator mareMediator) + { + _logger = logger; + _dalamudUtil = dalamudUtil; + _ipcManager = ipcManager; + _transientResourceManager = transientResourceManager; + _fileCacheManager = fileReplacementFactory; + _performanceCollector = performanceCollector; + _modelAnalyzer = modelAnalyzer; + _mareMediator = mareMediator; + _logger.LogTrace("Creating {this}", nameof(PlayerDataFactory)); + } + + public async Task BuildCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) + { + if (!_ipcManager.Initialized) + { + throw new InvalidOperationException("Penumbra or Glamourer is not connected"); + } + + if (playerRelatedObject == null) return; + + bool pointerIsZero = true; + try + { + pointerIsZero = playerRelatedObject.Address == IntPtr.Zero; + try + { + pointerIsZero = await CheckForNullDrawObject(playerRelatedObject.Address).ConfigureAwait(false); + } + catch + { + pointerIsZero = true; + _logger.LogDebug("NullRef for {object}", playerRelatedObject); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not create data for {object}", playerRelatedObject); + } + + if (pointerIsZero) + { + _logger.LogTrace("Pointer was zero for {objectKind}", playerRelatedObject.ObjectKind); + previousData.FileReplacements.Remove(playerRelatedObject.ObjectKind); + previousData.GlamourerString.Remove(playerRelatedObject.ObjectKind); + previousData.CustomizePlusScale.Remove(playerRelatedObject.ObjectKind); + return; + } + + var previousFileReplacements = previousData.FileReplacements.ToDictionary(d => d.Key, d => d.Value); + var previousGlamourerData = previousData.GlamourerString.ToDictionary(d => d.Key, d => d.Value); + var previousCustomize = previousData.CustomizePlusScale.ToDictionary(d => d.Key, d => d.Value); + + try + { + await _performanceCollector.LogPerformance(this, $"CreateCharacterData>{playerRelatedObject.ObjectKind}", async () => + { + await CreateCharacterData(previousData, playerRelatedObject, token).ConfigureAwait(false); + }).ConfigureAwait(true); + return; + } + catch (OperationCanceledException) + { + _logger.LogDebug("Cancelled creating Character data for {object}", playerRelatedObject); + throw; + } + catch (Exception e) + { + _logger.LogWarning(e, "Failed to create {object} data", playerRelatedObject); + } + + previousData.FileReplacements = previousFileReplacements; + previousData.GlamourerString = previousGlamourerData; + previousData.CustomizePlusScale = previousCustomize; + } + + private async Task CheckForNullDrawObject(IntPtr playerPointer) + { + return await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false); + } + + private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer) + { + return ((Character*)playerPointer)->GameObject.DrawObject == null; + } + + private async Task CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) + { + var objectKind = playerRelatedObject.ObjectKind; + + _logger.LogDebug("Building character data for {obj}", playerRelatedObject); + + if (!previousData.FileReplacements.TryGetValue(objectKind, out HashSet? value)) + { + previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance); + } + else + { + value.Clear(); + } + + previousData.CustomizePlusScale.Remove(objectKind); + + // wait until chara is not drawing and present so nothing spontaneously explodes + await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: token).ConfigureAwait(false); + int totalWaitTime = 10000; + while (!await _dalamudUtil.IsObjectPresentAsync(await _dalamudUtil.CreateGameObjectAsync(playerRelatedObject.Address).ConfigureAwait(false)).ConfigureAwait(false) && totalWaitTime > 0) + { + _logger.LogTrace("Character is null but it shouldn't be, waiting"); + await Task.Delay(50, token).ConfigureAwait(false); + totalWaitTime -= 50; + } + Dictionary>? boneIndices = + objectKind != ObjectKind.Player + ? null + : await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false); + + DateTime start = DateTime.UtcNow; + + // penumbra call, it's currently broken + Dictionary>? resolvedPaths; + + resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false); + if (resolvedPaths == null) throw new InvalidOperationException("Penumbra returned null data"); + + previousData.FileReplacements[objectKind] = + new HashSet(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance) + .Where(p => p.HasFileReplacement).ToHashSet(); + previousData.FileReplacements[objectKind].RemoveWhere(c => c.GamePaths.Any(g => !_allowedExtensionsForGamePaths.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); + + _logger.LogDebug("== Static Replacements =="); + foreach (var replacement in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase)) + { + _logger.LogDebug("=> {repl}", replacement); + } + + // if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times + // or we get into redraw city for every change and nothing works properly + if (objectKind == ObjectKind.Pet) + { + foreach (var item in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths)) + { + _logger.LogDebug("Persisting {item}", item); + _transientResourceManager.AddSemiTransientResource(objectKind, item); + } + } + + _logger.LogDebug("Handling transient update for {obj}", playerRelatedObject); + + // remove all potentially gathered paths from the transient resource manager that are resolved through static resolving + _transientResourceManager.ClearTransientPaths(playerRelatedObject.Address, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList()); + + // get all remaining paths and resolve them + var transientPaths = ManageSemiTransientData(objectKind, playerRelatedObject.Address); + 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)) + { + _logger.LogDebug("=> {repl}", replacement); + previousData.FileReplacements[objectKind].Add(replacement); + } + + // clean up all semi transient resources that don't have any file replacement (aka null resolve) + _transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. previousData.FileReplacements[objectKind]]); + + // make sure we only return data that actually has file replacements + foreach (var item in previousData.FileReplacements) + { + previousData.FileReplacements[item.Key] = new HashSet(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance); + } + + // gather up data from ipc + previousData.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations(); + Task getHeelsOffset = _ipcManager.Heels.GetOffsetAsync(); + Task getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address); + Task getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address); + Task getHonorificTitle = _ipcManager.Honorific.GetTitle(); + previousData.GlamourerString[playerRelatedObject.ObjectKind] = await getGlamourerData.ConfigureAwait(false); + _logger.LogDebug("Glamourer is now: {data}", previousData.GlamourerString[playerRelatedObject.ObjectKind]); + var customizeScale = await getCustomizeData.ConfigureAwait(false); + previousData.CustomizePlusScale[playerRelatedObject.ObjectKind] = customizeScale ?? string.Empty; + _logger.LogDebug("Customize is now: {data}", previousData.CustomizePlusScale[playerRelatedObject.ObjectKind]); + previousData.HonorificData = await getHonorificTitle.ConfigureAwait(false); + _logger.LogDebug("Honorific is now: {data}", previousData.HonorificData); + previousData.HeelsData = await getHeelsOffset.ConfigureAwait(false); + _logger.LogDebug("Heels is now: {heels}", previousData.HeelsData); + if (objectKind == ObjectKind.Player) + { + previousData.PetNamesData = _ipcManager.PetNames.GetLocalNames(); + _logger.LogDebug("Pet Nicknames is now: {petnames}", previousData.PetNamesData); + previousData.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty; + } + + if (previousData.FileReplacements.TryGetValue(objectKind, out HashSet? fileReplacements)) + { + var toCompute = fileReplacements.Where(f => !f.IsFileSwap).ToArray(); + _logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length); + var computedPaths = _fileCacheManager.GetFileCachesByPaths(toCompute.Select(c => c.ResolvedPath).ToArray()); + foreach (var file in toCompute) + { + file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty; + } + var removed = fileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash)); + if (removed > 0) + { + _logger.LogDebug("Removed {amount} of invalid files", removed); + } + } + + if (objectKind == ObjectKind.Player) + { + try + { + await VerifyPlayerAnimationBones(boneIndices, previousData, objectKind).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogWarning(e, "Failed to verify player animations, continuing without further verification"); + } + } + + _logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start.Ticks).TotalMilliseconds); + + return previousData; + } + + private async Task VerifyPlayerAnimationBones(Dictionary>? boneIndices, CharacterData previousData, ObjectKind objectKind) + { + if (boneIndices == null) return; + + 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; + + int noValidationFailed = 0; + foreach (var file in previousData.FileReplacements[objectKind].Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList()) + { + var skeletonIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(file.Hash)).ConfigureAwait(false); + bool validationFailed = false; + if (skeletonIndices != null) + { + // 105 is the maximum vanilla skellington spoopy bone index + if (skeletonIndices.All(k => k.Value.Max() <= 105)) + { + _logger.LogTrace("All indices of {path} are <= 105, ignoring", file.ResolvedPath); + continue; + } + + _logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count); + + foreach (var boneCount in skeletonIndices.Select(k => k).ToList()) + { + if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max()) + { + _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()); + validationFailed = true; + break; + } + } + } + + if (validationFailed) + { + noValidationFailed++; + _logger.LogDebug("Removing {file} from sent file replacements and transient data", file.ResolvedPath); + previousData.FileReplacements[objectKind].Remove(file); + foreach (var gamePath in file.GamePaths) + { + _transientResourceManager.RemoveTransientResource(objectKind, gamePath); + } + } + + } + + if (noValidationFailed > 0) + { + _mareMediator.Publish(new NotificationMessage("Invalid Skeleton Setup", + $"Your client is attempting to send {noValidationFailed} animation files with invalid bone data. Those animation files have been removed from your sent data. " + + $"Verify that you are using the correct skeleton for those animation files (Check /xllog for more information).", + NotificationType.Warning, TimeSpan.FromSeconds(10))); + } + } + + private async Task> GetFileReplacementsFromPaths(HashSet forwardResolve, HashSet reverseResolve) + { + var forwardPaths = forwardResolve.ToArray(); + var reversePaths = reverseResolve.ToArray(); + Dictionary> resolvedPaths = new(StringComparer.Ordinal); + var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false); + for (int i = 0; i < forwardPaths.Length; i++) + { + var filePath = forward[i].ToLowerInvariant(); + if (resolvedPaths.TryGetValue(filePath, out var list)) + { + list.Add(forwardPaths[i].ToLowerInvariant()); + } + else + { + resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()]; + } + } + + for (int i = 0; i < reversePaths.Length; i++) + { + var filePath = reversePaths[i].ToLowerInvariant(); + if (resolvedPaths.TryGetValue(filePath, out var list)) + { + list.AddRange(reverse[i].Select(c => c.ToLowerInvariant())); + } + else + { + resolvedPaths[filePath] = new List(reverse[i].Select(c => c.ToLowerInvariant()).ToList()); + } + } + + return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly(); + } + + private HashSet ManageSemiTransientData(ObjectKind objectKind, IntPtr charaPointer) + { + _transientResourceManager.PersistTransientResources(charaPointer, objectKind); + + HashSet pathsToResolve = new(StringComparer.Ordinal); + foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path))) + { + pathsToResolve.Add(path); + } + + return pathsToResolve; + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs b/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs new file mode 100644 index 0000000..2e9fc66 --- /dev/null +++ b/MareSynchronos/PlayerData/Handlers/GameObjectHandler.cs @@ -0,0 +1,487 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; +using System.Runtime.CompilerServices; +using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer; +using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind; + +namespace MareSynchronos.PlayerData.Handlers; + +public sealed class GameObjectHandler : DisposableMediatorSubscriberBase +{ + private readonly DalamudUtilService _dalamudUtil; + private readonly Func _getAddress; + private readonly bool _isOwnedObject; + private readonly PerformanceCollectorService _performanceCollector; + private CancellationTokenSource? _clearCts = new(); + private Task? _delayedZoningTask; + private bool _haltProcessing = false; + private bool _ignoreSendAfterRedraw = false; + private int _ptrNullCounter = 0; + private byte _classJob = 0; + private CancellationTokenSource _zoningCts = new(); + + public GameObjectHandler(ILogger logger, PerformanceCollectorService performanceCollector, + MareMediator mediator, DalamudUtilService dalamudUtil, ObjectKind objectKind, Func getAddress, bool ownedObject = true) : base(logger, mediator) + { + _performanceCollector = performanceCollector; + ObjectKind = objectKind; + _dalamudUtil = dalamudUtil; + _getAddress = () => + { + _dalamudUtil.EnsureIsOnFramework(); + return getAddress.Invoke(); + }; + _isOwnedObject = ownedObject; + Name = string.Empty; + + if (ownedObject) + { + Mediator.Subscribe(this, (msg) => + { + if (_delayedZoningTask?.IsCompleted ?? true) + { + if (msg.Address != Address) return; + Mediator.Publish(new CreateCacheForObjectMessage(this)); + } + }); + } + + Mediator.Subscribe(this, (_) => FrameworkUpdate()); + + Mediator.Subscribe(this, (_) => ZoneSwitchEnd()); + Mediator.Subscribe(this, (_) => ZoneSwitchStart()); + + Mediator.Subscribe(this, (_) => + { + _haltProcessing = true; + }); + Mediator.Subscribe(this, (_) => + { + _haltProcessing = false; + ZoneSwitchEnd(); + }); + Mediator.Subscribe(this, (msg) => + { + if (msg.Address == Address) + { + _haltProcessing = true; + } + }); + Mediator.Subscribe(this, (msg) => + { + if (msg.Address == Address) + { + _haltProcessing = false; + _ = Task.Run(async () => + { + _ignoreSendAfterRedraw = true; + await Task.Delay(500).ConfigureAwait(false); + _ignoreSendAfterRedraw = false; + }); + } + }); + + Mediator.Publish(new GameObjectHandlerCreatedMessage(this, _isOwnedObject)); + + _dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult(); + } + + private enum DrawCondition + { + None, + DrawObjectZero, + RenderFlags, + ModelInSlotLoaded, + ModelFilesInSlotLoaded + } + + public byte RaceId { get; private set; } + public byte Gender { get; private set; } + public byte TribeId { get; private set; } + + public IntPtr Address { get; private set; } + public string Name { get; private set; } + public ObjectKind ObjectKind { get; } + private byte[] CustomizeData { get; set; } = new byte[26]; + private IntPtr DrawObjectAddress { get; set; } + private byte[] EquipSlotData { get; set; } = new byte[40]; + private ushort[] MainHandData { get; set; } = new ushort[3]; + private ushort[] OffHandData { get; set; } = new ushort[3]; + + public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action act, CancellationToken token) + { + while (await _dalamudUtil.RunOnFrameworkThread(() => + { + if (IsBeingDrawn()) return true; + var gameObj = _dalamudUtil.CreateGameObject(Address); + if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara) + { + act.Invoke(chara); + } + return false; + }).ConfigureAwait(false)) + { + await Task.Delay(250, token).ConfigureAwait(false); + } + } + + public void CompareNameAndThrow(string name) + { + if (!string.Equals(Name, name, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("Player name not equal to requested name, pointer invalid"); + } + if (Address == IntPtr.Zero) + { + throw new InvalidOperationException("Player pointer is zero, pointer invalid"); + } + } + + public IntPtr CurrentAddress() + { + _dalamudUtil.EnsureIsOnFramework(); + return _getAddress.Invoke(); + } + + public Dalamud.Game.ClientState.Objects.Types.IGameObject? GetGameObject() + { + return _dalamudUtil.CreateGameObject(Address); + } + + public void Invalidate() + { + Address = IntPtr.Zero; + DrawObjectAddress = IntPtr.Zero; + _haltProcessing = false; + } + + public async Task IsBeingDrawnRunOnFrameworkAsync() + { + return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false); + } + + public override string ToString() + { + var owned = _isOwnedObject ? "Self" : "Other"; + return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})"; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + Mediator.Publish(new GameObjectHandlerDestroyedMessage(this, _isOwnedObject)); + } + + private unsafe void CheckAndUpdateObject() + { + var prevAddr = Address; + var prevDrawObj = DrawObjectAddress; + + Address = _getAddress(); + if (Address != IntPtr.Zero) + { + _ptrNullCounter = 0; + var drawObjAddr = (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->DrawObject; + DrawObjectAddress = drawObjAddr; + } + else + { + DrawObjectAddress = IntPtr.Zero; + } + + if (_haltProcessing) return; + + bool drawObjDiff = DrawObjectAddress != prevDrawObj; + bool addrDiff = Address != prevAddr; + + if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero) + { + if (_clearCts != null) + { + Logger.LogDebug("[{this}] Cancelling Clear Task", this); + _clearCts.CancelDispose(); + _clearCts = null; + } + var chara = (Character*)Address; + var name = chara->GameObject.NameString; + bool nameChange = !string.Equals(name, Name, StringComparison.Ordinal); + if (nameChange) + { + Name = name; + } + bool equipDiff = false; + + if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase + && ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human) + { + var classJob = chara->CharacterData.ClassJob; + if (classJob != _classJob) + { + Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob); + _classJob = classJob; + Mediator.Publish(new ClassJobChangedMessage(this)); + } + + equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)DrawObjectAddress)->Head); + + ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand); + ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand); + equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject); + equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject); + + if (equipDiff) + Logger.LogTrace("Checking [{this}] equip data as human from draw obj, result: {diff}", this, equipDiff); + } + else + { + equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0])); + if (equipDiff) + Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff); + } + + if (equipDiff && !_isOwnedObject && !_ignoreSendAfterRedraw) // send the message out immediately and cancel out, no reason to continue if not self + { + Logger.LogTrace("[{this}] Changed", this); + Mediator.Publish(new CharacterChangedMessage(this)); + return; + } + + bool customizeDiff = false; + + if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase + && ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human) + { + var gender = ((Human*)DrawObjectAddress)->Customize.Sex; + var raceId = ((Human*)DrawObjectAddress)->Customize.Race; + var tribeId = ((Human*)DrawObjectAddress)->Customize.Tribe; + + if (_isOwnedObject && ObjectKind == ObjectKind.Player + && (gender != Gender || raceId != RaceId || tribeId != TribeId)) + { + Mediator.Publish(new CensusUpdateMessage(gender, raceId, tribeId)); + Gender = gender; + RaceId = raceId; + TribeId = tribeId; + } + + customizeDiff = CompareAndUpdateCustomizeData(((Human*)DrawObjectAddress)->Customize.Data); + if (customizeDiff) + Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff); + } + else + { + customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data); + if (customizeDiff) + Logger.LogTrace("Checking [{this}] customize data from game obj, result: {diff}", this, equipDiff); + } + + if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject) + { + Logger.LogDebug("[{this}] Changed, Sending CreateCacheObjectMessage", this); + Mediator.Publish(new CreateCacheForObjectMessage(this)); + } + } + else if (addrDiff || drawObjDiff) + { + Logger.LogTrace("[{this}] Changed", this); + if (_isOwnedObject && ObjectKind != ObjectKind.Player) + { + _clearCts?.CancelDispose(); + _clearCts = new(); + var token = _clearCts.Token; + _ = Task.Run(() => ClearAsync(token), token); + } + } + } + + private async Task ClearAsync(CancellationToken token) + { + Logger.LogDebug("[{this}] Running Clear Task", this); + await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); + Logger.LogDebug("[{this}] Sending ClearCachedForObjectMessage", this); + Mediator.Publish(new ClearCacheForObjectMessage(this)); + _clearCts = null; + } + + private unsafe bool CompareAndUpdateCustomizeData(Span customizeData) + { + bool hasChanges = false; + + for (int i = 0; i < customizeData.Length; i++) + { + var data = customizeData[i]; + if (CustomizeData[i] != data) + { + CustomizeData[i] = data; + hasChanges = true; + } + } + + return hasChanges; + } + + private unsafe bool CompareAndUpdateEquipByteData(byte* equipSlotData) + { + bool hasChanges = false; + for (int i = 0; i < EquipSlotData.Length; i++) + { + var data = equipSlotData[i]; + if (EquipSlotData[i] != data) + { + EquipSlotData[i] = data; + hasChanges = true; + } + } + + return hasChanges; + } + + private unsafe bool CompareAndUpdateMainHand(Weapon* weapon) + { + if ((nint)weapon == nint.Zero) return false; + bool hasChanges = false; + hasChanges |= weapon->ModelSetId != MainHandData[0]; + MainHandData[0] = weapon->ModelSetId; + hasChanges |= weapon->Variant != MainHandData[1]; + MainHandData[1] = weapon->Variant; + hasChanges |= weapon->SecondaryId != MainHandData[2]; + MainHandData[2] = weapon->SecondaryId; + return hasChanges; + } + + private unsafe bool CompareAndUpdateOffHand(Weapon* weapon) + { + if ((nint)weapon == nint.Zero) return false; + bool hasChanges = false; + hasChanges |= weapon->ModelSetId != OffHandData[0]; + OffHandData[0] = weapon->ModelSetId; + hasChanges |= weapon->Variant != OffHandData[1]; + OffHandData[1] = weapon->Variant; + hasChanges |= weapon->SecondaryId != OffHandData[2]; + OffHandData[2] = weapon->SecondaryId; + return hasChanges; + } + + private void FrameworkUpdate() + { + if (!_delayedZoningTask?.IsCompleted ?? false) return; + + try + { + _performanceCollector.LogPerformance(this, $"CheckAndUpdateObject>{(_isOwnedObject ? "Self" : "Other")}+{ObjectKind}/{(string.IsNullOrEmpty(Name) ? "Unk" : Name)}" + + $"+{Address.ToString("X")}", CheckAndUpdateObject); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error during FrameworkUpdate of {this}", this); + } + } + + private unsafe IntPtr GetDrawObjUnsafe(nint curPtr) + { + return (IntPtr)((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)curPtr)->DrawObject; + } + + private bool IsBeingDrawn() + { + var curPtr = _getAddress(); + Logger.LogTrace("[{this}] IsBeingDrawn, CurPtr: {ptr}", this, curPtr.ToString("X")); + + if (curPtr == IntPtr.Zero && _ptrNullCounter < 2) + { + Logger.LogTrace("[{this}] IsBeingDrawn, CurPtr is ZERO, counter is {cnt}", this, _ptrNullCounter); + _ptrNullCounter++; + return true; + } + + if (curPtr == IntPtr.Zero) + { + Logger.LogTrace("[{this}] IsBeingDrawn, CurPtr is ZERO, returning", this); + + Address = IntPtr.Zero; + DrawObjectAddress = IntPtr.Zero; + throw new ArgumentNullException($"CurPtr for {this} turned ZERO"); + } + + if (_dalamudUtil.IsAnythingDrawing) + { + Logger.LogTrace("[{this}] IsBeingDrawn, Global draw block", this); + return true; + } + + var drawObj = GetDrawObjUnsafe(curPtr); + Logger.LogTrace("[{this}] IsBeingDrawn, DrawObjPtr: {ptr}", this, drawObj.ToString("X")); + var isDrawn = IsBeingDrawnUnsafe(drawObj, curPtr); + Logger.LogTrace("[{this}] IsBeingDrawn, Condition: {cond}", this, isDrawn); + return isDrawn != DrawCondition.None; + } + + private unsafe DrawCondition IsBeingDrawnUnsafe(IntPtr drawObj, IntPtr curPtr) + { + var drawObjZero = drawObj == IntPtr.Zero; + if (drawObjZero) return DrawCondition.DrawObjectZero; + var renderFlags = (((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)curPtr)->RenderFlags) != 0x0; + if (renderFlags) return DrawCondition.RenderFlags; + + if (ObjectKind == ObjectKind.Player) + { + var modelInSlotLoaded = (((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0); + if (modelInSlotLoaded) return DrawCondition.ModelInSlotLoaded; + var modelFilesInSlotLoaded = (((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0); + if (modelFilesInSlotLoaded) return DrawCondition.ModelFilesInSlotLoaded; + return DrawCondition.None; + } + + return DrawCondition.None; + } + + private void ZoneSwitchEnd() + { + if (!_isOwnedObject || _haltProcessing) return; + + _clearCts?.Cancel(); + _clearCts?.Dispose(); + _clearCts = null; + try + { + _zoningCts?.CancelAfter(2500); + } + catch (ObjectDisposedException) + { + // ignore + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Zoning CTS cancel issue"); + } + } + + private void ZoneSwitchStart() + { + if (!_isOwnedObject || _haltProcessing) return; + + _zoningCts = new(); + Logger.LogDebug("[{obj}] Starting Delay After Zoning", this); + _delayedZoningTask = Task.Run(async () => + { + try + { + await Task.Delay(TimeSpan.FromSeconds(120), _zoningCts.Token).ConfigureAwait(false); + } + catch + { + // ignore cancelled + } + finally + { + Logger.LogDebug("[{this}] Delay after zoning complete", this); + _zoningCts.Dispose(); + } + }); + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Handlers/PairHandler.cs b/MareSynchronos/PlayerData/Handlers/PairHandler.cs new file mode 100644 index 0000000..26e3aee --- /dev/null +++ b/MareSynchronos/PlayerData/Handlers/PairHandler.cs @@ -0,0 +1,890 @@ +using MareSynchronos.API.Data; +using MareSynchronos.FileCache; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Factories; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Events; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI.Files; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Diagnostics; +using ObjectKind = MareSynchronos.API.Data.Enum.ObjectKind; + +namespace MareSynchronos.PlayerData.Handlers; + +public sealed class PairHandler : DisposableMediatorSubscriberBase +{ + private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced); + + private readonly MareConfigService _configService; + private readonly DalamudUtilService _dalamudUtil; + private readonly FileDownloadManager _downloadManager; + private readonly FileCacheManager _fileDbManager; + private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; + private readonly IpcManager _ipcManager; + private readonly PlayerPerformanceService _playerPerformanceService; + private readonly ServerConfigurationManager _serverConfigManager; + private readonly PluginWarningNotificationService _pluginWarningNotificationManager; + private readonly VisibilityService _visibilityService; + 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 _deferred = Guid.Empty; + private Guid _penumbraCollection = Guid.Empty; + private bool _redrawOnNextApplication = false; + + public PairHandler(ILogger logger, Pair pair, PairAnalyzer pairAnalyzer, + GameObjectHandlerFactory gameObjectHandlerFactory, + IpcManager ipcManager, FileDownloadManager transferManager, + PluginWarningNotificationService pluginWarningNotificationManager, + DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime, + FileCacheManager fileDbManager, MareMediator mediator, + PlayerPerformanceService playerPerformanceService, + ServerConfigurationManager serverConfigManager, + MareConfigService configService, VisibilityService visibilityService) : base(logger, mediator) + { + Pair = pair; + PairAnalyzer = pairAnalyzer; + _gameObjectHandlerFactory = gameObjectHandlerFactory; + _ipcManager = ipcManager; + _downloadManager = transferManager; + _pluginWarningNotificationManager = pluginWarningNotificationManager; + _dalamudUtil = dalamudUtil; + _fileDbManager = fileDbManager; + _playerPerformanceService = playerPerformanceService; + _serverConfigManager = serverConfigManager; + _configService = configService; + _visibilityService = visibilityService; + + _visibilityService.StartTracking(Pair.Ident); + + Mediator.SubscribeKeyed(this, Pair.Ident, (msg) => UpdateVisibility(msg.IsVisible, msg.Invalidate)); + + Mediator.Subscribe(this, (_) => + { + _downloadCancellationTokenSource?.CancelDispose(); + _charaHandler?.Invalidate(); + IsVisible = false; + }); + Mediator.Subscribe(this, (_) => + { + _penumbraCollection = Guid.Empty; + 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) => + { + if (IsVisible && _dataReceivedInDowntime != null) + { + ApplyCharacterData(_dataReceivedInDowntime.ApplicationId, + _dataReceivedInDowntime.CharacterData, _dataReceivedInDowntime.Forced); + _dataReceivedInDowntime = null; + } + }); + Mediator.Subscribe(this, _ => + { + if (_configService.Current.HoldCombatApplication) + { + _dataReceivedInDowntime = null; + _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate(); + _applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate(); + } + }); + Mediator.Subscribe(this, (msg) => + { + if (msg.UID != null && !msg.UID.Equals(Pair.UserData.UID, StringComparison.Ordinal)) return; + Logger.LogDebug("Recalculating performance for {uid}", Pair.UserData.UID); + pair.ApplyLastReceivedData(forced: true); + }); + + 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))); + } + } + } + + public long LastAppliedDataBytes { get; private set; } + public Pair Pair { get; private init; } + public PairAnalyzer PairAnalyzer { get; private init; } + 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 (_configService.Current.HoldCombatApplication && _dalamudUtil.IsInCombatOrPerforming) + { + Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning, + "Cannot apply character data: you are in combat or performing music, deferring application"))); + Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat or performing", applicationBase); + _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); + SetUploading(isUploading: false); + return; + } + + if (_charaHandler == null || (PlayerCharacter == IntPtr.Zero)) + { + if (_deferred != Guid.Empty) + { + _isVisible = false; + _visibilityService.StopTracking(Pair.Ident); + _visibilityService.StartTracking(Pair.Ident); + } + + 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; + Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, characterData)); + Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods); + // Ensure that this deferred application actually occurs by forcing visibiltiy to re-proc + // Set _deferred as a silencing flag to avoid spamming logs once per frame with failed applications + _isVisible = false; + _deferred = applicationBase; + _visibilityService.StopTracking(Pair.Ident); + _visibilityService.StartTracking(Pair.Ident); + return; + } + + _deferred = Guid.Empty; + + SetUploading(isUploading: false); + + if (Pair.IsDownloadBlocked) + { + var reasons = string.Join(", ", Pair.HoldDownloadReasons); + Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning, + $"Not applying character data: {reasons}"))); + Logger.LogDebug("[BASE-{appBase}] Not applying due to hold: {reasons}", applicationBase, reasons); + 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; + Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, characterData)); + Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods); + return; + } + + 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); + + if (!disposing) return; + + _visibilityService.StopTracking(Pair.Ident); + + SetUploading(isUploading: false); + var name = PlayerName; + Logger.LogDebug("Disposing {name} ({user})", name, Pair); + try + { + Guid applicationId = Guid.NewGuid(); + + if (!string.IsNullOrEmpty(name)) + { + Mediator.Publish(new EventMessage(new Event(name, Pair.UserData, nameof(PairHandler), EventSeverity.Informational, "Disposing User"))); + } + + UndoApplicationAsync(applicationId).GetAwaiter().GetResult(); + + _applicationCancellationTokenSource?.Dispose(); + _applicationCancellationTokenSource = null; + _downloadCancellationTokenSource?.Dispose(); + _downloadCancellationTokenSource = null; + _charaHandler?.Dispose(); + _charaHandler = null; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error on disposal of {name}", name); + } + finally + { + PlayerName = null; + _cachedData = null; + Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, null)); + Logger.LogDebug("Disposing {name} complete", name); + } + } + + public void UndoApplication(Guid applicationId = default) + { + _ = Task.Run(async () => { + await UndoApplicationAsync(applicationId).ConfigureAwait(false); + }); + } + + private async Task UndoApplicationAsync(Guid applicationId = default) + { + Logger.LogDebug($"Undoing application of {Pair.UserPair}"); + var name = PlayerName; + try + { + if (applicationId == default) + applicationId = Guid.NewGuid(); + _applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate(); + _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate(); + + Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, Pair.UserPair); + if (_penumbraCollection != Guid.Empty) + { + await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).ConfigureAwait(false); + _penumbraCollection = Guid.Empty; + } + + if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name)) + { + Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, Pair.UserPair); + if (!IsVisible) + { + Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, Pair.UserPair); + await _ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).ConfigureAwait(false); + } + 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 + { + await RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).ConfigureAwait(false); + } + catch (InvalidOperationException ex) + { + Logger.LogWarning(ex, "Failed disposing player (not present anymore?)"); + break; + } + } + } + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error on undoing application of {name}", 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.GetCompanion(ptr), isWatched: false).ConfigureAwait(false), + ObjectKind.MinionOrMount => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetMinionOrMount(ptr), isWatched: false).ConfigureAwait(false), + ObjectKind.Pet => await _gameObjectHandlerFactory.Create(changes.Key, () => _dalamudUtil.GetPet(ptr), isWatched: false).ConfigureAwait(false), + _ => throw new NotSupportedException("ObjectKind not supported: " + changes.Key) + }; + + async Task processApplication(IEnumerable changeList) + { + foreach (var change in changeList) + { + Logger.LogDebug("[{applicationId}{ft}] Processing {change} for {handler}", applicationId, _dalamudUtil.IsOnFrameworkThread ? "*" : "", 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, allowImmediate: true).ConfigureAwait(false); + } + break; + + case PlayerChanges.PetNames: + await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false); + break; + + case PlayerChanges.Moodles: + await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false); + break; + + case PlayerChanges.ForcedRedraw: + await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false); + break; + + default: + break; + } + token.ThrowIfCancellationRequested(); + } + } + + 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(); + if (_configService.Current.SerialApplication) + { + var serialChangeList = changes.Value.Where(p => p <= PlayerChanges.ForcedRedraw).OrderBy(p => (int)p); + var asyncChangeList = changes.Value.Where(p => p > PlayerChanges.ForcedRedraw).OrderBy(p => (int)p); + await _dalamudUtil.RunOnFrameworkThread(async () => await processApplication(serialChangeList).ConfigureAwait(false)).ConfigureAwait(false); + await Task.Run(async () => await processApplication(asyncChangeList).ConfigureAwait(false), CancellationToken.None).ConfigureAwait(false); + } + else + { + _ = processApplication(changes.Value.OrderBy(p => (int)p)); + } + } + 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; + + _ = Task.Run(async () => { + await 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) + { + Logger.LogTrace("[BASE-{appBase}] DownloadAndApplyCharacterAsync", applicationBase); + Dictionary<(string GamePath, string? Hash), string> moddedPaths = []; + + if (updateModdedPaths) + { + Logger.LogTrace("[BASE-{appBase}] DownloadAndApplyCharacterAsync > updateModdedPaths", applicationBase); + 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)) + { + Pair.HoldApplication("IndividualPerformanceThreshold", maxValue: 1); + _downloadManager.ClearDownload(); + return; + } + + _pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false), downloadToken); + + 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); + } + + try + { + Mediator.Publish(new HaltScanMessage(nameof(PlayerPerformanceService.ShrinkTextures))); + if (await _playerPerformanceService.ShrinkTextures(this, charaData, downloadToken).ConfigureAwait(false)) + _ = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); + } + finally + { + Mediator.Publish(new ResumeScanMessage(nameof(PlayerPerformanceService.ShrinkTextures))); + } + + bool exceedsThreshold = !await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false); + + if (exceedsThreshold) + Pair.HoldApplication("IndividualPerformanceThreshold", maxValue: 1); + else + Pair.UnholdApplication("IndividualPerformanceThreshold"); + + if (exceedsThreshold) + { + Logger.LogTrace("[BASE-{appBase}] Not applying due to performance thresholds", applicationBase); + return; + } + } + + if (Pair.IsApplicationBlocked) + { + var reasons = string.Join(", ", Pair.HoldApplicationReasons); + Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning, + $"Not applying character data: {reasons}"))); + Logger.LogTrace("[BASE-{appBase}] Not applying due to hold: {reasons}", applicationBase, reasons); + 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) + { + ushort objIndex = ushort.MaxValue; + try + { + _applicationId = Guid.NewGuid(); + Logger.LogDebug("[BASE-{applicationId}] Starting application task for {this}: {appId}", applicationBase, this, _applicationId); + + if (_penumbraCollection == Guid.Empty) + { + if (objIndex == ushort.MaxValue) + objIndex = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler!.GetGameObject()!.ObjectIndex).ConfigureAwait(false); + _penumbraCollection = await _ipcManager.Penumbra.CreateTemporaryCollectionAsync(Logger, Pair.UserData.UID).ConfigureAwait(false); + await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, _penumbraCollection, objIndex).ConfigureAwait(false); + } + + 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 + if (objIndex == ushort.MaxValue) + 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; + Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, 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; + Mediator.Publish(new PairDataAppliedMessage(Pair.UserData.UID, charaData)); + Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId); + } + else + { + Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId); + } + } + } + + private void UpdateVisibility(bool nowVisible, bool invalidate = false) + { + if (string.IsNullOrEmpty(PlayerName)) + { + var pc = _dalamudUtil.FindPlayerByNameHash(Pair.Ident); + if (pc.ObjectId == 0) 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}"))); + } + + // This was triggered by the character becoming handled by Mare, so unapply everything + // There seems to be a good chance that this races Mare and then crashes + if (!nowVisible && invalidate) + { + bool wasVisible = IsVisible; + IsVisible = false; + _charaHandler?.Invalidate(); + _downloadCancellationTokenSource?.CancelDispose(); + _downloadCancellationTokenSource = null; + if (wasVisible) + Logger.LogTrace("{this} visibility changed, now: {visi}", this, IsVisible); + Logger.LogDebug("Invalidating {this}", this); + UndoApplication(); + return; + } + + if (!IsVisible && nowVisible) + { + // This is deferred application attempt, avoid any log output + if (_deferred != Guid.Empty) + { + _isVisible = true; + _ = Task.Run(() => + { + ApplyCharacterData(_deferred, _cachedData!, forceApplyCustomization: true); + }); + } + + IsVisible = true; + Mediator.Publish(new PairHandlerVisibleMessage(this)); + if (_cachedData != null) + { + Guid appData = Guid.NewGuid(); + 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 (IsVisible && !nowVisible) + { + 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(); + + Mediator.Subscribe(this, msg => + { + if (string.IsNullOrEmpty(_cachedData?.HonorificData)) return; + Logger.LogTrace("Reapplying Honorific data for {this}", this); + _ = Task.Run(async () => await _ipcManager.Honorific.SetTitleAsync(PlayerCharacter, _cachedData.HonorificData).ConfigureAwait(false), CancellationToken.None); + }); + + Mediator.Subscribe(this, msg => + { + if (string.IsNullOrEmpty(_cachedData?.PetNamesData)) return; + Logger.LogTrace("Reapplying Pet Names data for {this}", this); + _ = Task.Run(async () => await _ipcManager.PetNames.SetPlayerData(PlayerCharacter, _cachedData.PetNamesData).ConfigureAwait(false), CancellationToken.None); + }); + } + + 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 Pet Nicknames for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name); + await _ipcManager.PetNames.ClearPlayerData(address).ConfigureAwait(false); + Logger.LogDebug("[{applicationId}] Restoring Moodles for {alias}/{name}", applicationId, Pair.UserData.AliasOrUID, name); + await _ipcManager.Moodles.RevertStatusAsync(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, preferSubst: true); + 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) + { + 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]; + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs b/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs new file mode 100644 index 0000000..7655b72 --- /dev/null +++ b/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs @@ -0,0 +1,75 @@ +using MareSynchronos.API.Data; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using MareSynchronos.WebAPI.Files; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.PlayerData.Pairs; + +public class OnlinePlayerManager : DisposableMediatorSubscriberBase +{ + private readonly ApiController _apiController; + private readonly DalamudUtilService _dalamudUtil; + private readonly FileUploadManager _fileTransferManager; + private readonly HashSet _newVisiblePlayers = []; + private readonly PairManager _pairManager; + private CharacterData? _lastSentData; + + public OnlinePlayerManager(ILogger logger, ApiController apiController, DalamudUtilService dalamudUtil, + PairManager pairManager, MareMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator) + { + _apiController = apiController; + _dalamudUtil = dalamudUtil; + _pairManager = pairManager; + _fileTransferManager = fileTransferManager; + Mediator.Subscribe(this, (_) => PlayerManagerOnPlayerHasChanged()); + Mediator.Subscribe(this, (_) => FrameworkOnUpdate()); + Mediator.Subscribe(this, (msg) => + { + var newData = msg.CharacterData; + if (_lastSentData == null || (!string.Equals(newData.DataHash.Value, _lastSentData.DataHash.Value, StringComparison.Ordinal))) + { + Logger.LogDebug("Pushing data for visible players"); + _lastSentData = newData; + PushCharacterData(_pairManager.GetVisibleUsers()); + } + else + { + Logger.LogDebug("Not sending data for {hash}", newData.DataHash.Value); + } + }); + Mediator.Subscribe(this, (msg) => _newVisiblePlayers.Add(msg.Player)); + Mediator.Subscribe(this, (_) => PushCharacterData(_pairManager.GetVisibleUsers())); + } + + private void FrameworkOnUpdate() + { + if (!_dalamudUtil.GetIsPlayerPresent() || !_apiController.IsConnected) return; + + if (!_newVisiblePlayers.Any()) return; + var newVisiblePlayers = _newVisiblePlayers.ToList(); + _newVisiblePlayers.Clear(); + Logger.LogTrace("Has new visible players, pushing character data"); + PushCharacterData(newVisiblePlayers.Select(c => c.Pair.UserData).ToList()); + } + + private void PlayerManagerOnPlayerHasChanged() + { + PushCharacterData(_pairManager.GetVisibleUsers()); + } + + private void PushCharacterData(List visiblePlayers) + { + if (visiblePlayers.Any() && _lastSentData != null) + { + _ = Task.Run(async () => + { + var dataToSend = await _fileTransferManager.UploadFiles(_lastSentData.DeepClone(), visiblePlayers).ConfigureAwait(false); + await _apiController.PushCharacterData(dataToSend, visiblePlayers).ConfigureAwait(false); + }); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs b/MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs new file mode 100644 index 0000000..a52467b --- /dev/null +++ b/MareSynchronos/PlayerData/Pairs/OptionalPluginWarning.cs @@ -0,0 +1,10 @@ +namespace MareSynchronos.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 ShowPetNicknamesWarning { get; set; } = false; + public bool ShownMoodlesWarning { get; set; } = false; +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Pairs/Pair.cs b/MareSynchronos/PlayerData/Pairs/Pair.cs new file mode 100644 index 0000000..1ca50bc --- /dev/null +++ b/MareSynchronos/PlayerData/Pairs/Pair.cs @@ -0,0 +1,371 @@ +using Dalamud.Game.Gui.ContextMenu; +using Dalamud.Game.Text.SeStringHandling; +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Comparer; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Factories; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace MareSynchronos.PlayerData.Pairs; + +public class Pair : DisposableMediatorSubscriberBase +{ + private readonly PairHandlerFactory _cachedPlayerFactory; + private readonly SemaphoreSlim _creationSemaphore = new(1); + private readonly ILogger _logger; + private readonly MareConfigService _mareConfig; + private readonly ServerConfigurationManager _serverConfigurationManager; + private CancellationTokenSource _applicationCts = new(); + private OnlineUserIdentDto? _onlineUserIdentDto = null; + + public Pair(ILogger logger, UserData userData, PairHandlerFactory cachedPlayerFactory, + MareMediator mediator, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) + : base(logger, mediator) + { + _logger = logger; + _cachedPlayerFactory = cachedPlayerFactory; + _mareConfig = mareConfig; + _serverConfigurationManager = serverConfigurationManager; + + UserData = userData; + + Mediator.SubscribeKeyed(this, UserData.UID, (msg) => HoldApplication(msg.Source)); + Mediator.SubscribeKeyed(this, UserData.UID, (msg) => UnholdApplication(msg.Source)); + } + + public Dictionary GroupPair { get; set; } = new(GroupDtoComparer.Instance); + public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName) && _onlineUserIdentDto != null; + public bool IsOnline => CachedPlayer != null; + + public bool IsPaused => UserPair != null && UserPair.OtherPermissions.IsPaired() ? UserPair.OtherPermissions.IsPaused() || UserPair.OwnPermissions.IsPaused() + : GroupPair.All(p => p.Key.GroupUserPermissions.IsPaused() || p.Value.GroupUserPermissions.IsPaused()); + + // Download locks apply earlier in the process than Application locks + private ConcurrentDictionary HoldDownloadLocks { get; set; } = new(StringComparer.Ordinal); + private ConcurrentDictionary HoldApplicationLocks { get; set; } = new(StringComparer.Ordinal); + + public bool IsDownloadBlocked => HoldDownloadLocks.Any(f => f.Value > 0); + public bool IsApplicationBlocked => HoldApplicationLocks.Any(f => f.Value > 0) || IsDownloadBlocked; + + public IEnumerable HoldDownloadReasons => HoldDownloadLocks.Keys; + public IEnumerable HoldApplicationReasons => Enumerable.Concat(HoldDownloadLocks.Keys, HoldApplicationLocks.Keys); + + public bool IsVisible => CachedPlayer?.IsVisible ?? false; + public CharacterData? LastReceivedCharacterData { get; set; } + public string? PlayerName => GetPlayerName(); + public uint PlayerCharacterId => GetPlayerCharacterId(); + 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 PairAnalyzer? PairAnalyzer => CachedPlayer?.PairAnalyzer; + + public UserData UserData { get; init; } + + public UserPairDto? 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; + + void Add(string name, Action? action) + { + args.AddMenuItem(new MenuItem() + { + Name = name, + OnClicked = action, + PrefixColor = 526, + PrefixChar = 'S' + }); + } + + bool isBlocked = IsApplicationBlocked; + bool isBlacklisted = _serverConfigurationManager.IsUidBlacklisted(UserData.UID); + bool isWhitelisted = _serverConfigurationManager.IsUidWhitelisted(UserData.UID); + + Add("Open Profile", _ => Mediator.Publish(new ProfileOpenStandaloneMessage(this))); + + if (!isBlocked && !isBlacklisted) + Add("Always Block Modded Appearance", _ => { + _serverConfigurationManager.AddBlacklistUid(UserData.UID); + HoldApplication("Blacklist", maxValue: 1); + ApplyLastReceivedData(forced: true); + }); + else if (isBlocked && !isWhitelisted) + Add("Always Allow Modded Appearance", _ => { + _serverConfigurationManager.AddWhitelistUid(UserData.UID); + UnholdApplication("Blacklist", skipApplication: true); + ApplyLastReceivedData(forced: true); + }); + + if (isWhitelisted) + Add("Remove from Whitelist", _ => { + _serverConfigurationManager.RemoveWhitelistUid(UserData.UID); + ApplyLastReceivedData(forced: true); + }); + else if (isBlacklisted) + Add("Remove from Blacklist", _ => { + _serverConfigurationManager.RemoveBlacklistUid(UserData.UID); + UnholdApplication("Blacklist", skipApplication: true); + ApplyLastReceivedData(forced: true); + }); + + Add("Reapply last data", _ => ApplyLastReceivedData(forced: true)); + + if (UserPair != null) + { + Add("Change Permissions", _ => Mediator.Publish(new OpenPermissionWindow(this))); + Add("Cycle pause state", _ => Mediator.Publish(new CyclePauseMessage(UserData))); + } + } + + public void ApplyData(OnlineUserCharaDataDto data) + { + _applicationCts = _applicationCts.CancelRecreate(); + LastReceivedCharacterData = data.CharaData; + + if (CachedPlayer == null) + { + _logger.LogDebug("Received Data for {uid} but CachedPlayer does not exist, waiting", data.User.UID); + _ = Task.Run(async () => + { + using var timeoutCts = new CancellationTokenSource(); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(120)); + var appToken = _applicationCts.Token; + using var combined = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, appToken); + while (CachedPlayer == null && !combined.Token.IsCancellationRequested) + { + await Task.Delay(250, combined.Token).ConfigureAwait(false); + } + + if (!combined.IsCancellationRequested) + { + _logger.LogDebug("Applying delayed data for {uid}", data.User.UID); + ApplyLastReceivedData(); + } + }); + return; + } + + ApplyLastReceivedData(); + } + + public void ApplyLastReceivedData(bool forced = false) + { + if (CachedPlayer == null) return; + if (LastReceivedCharacterData == null) return; + if (IsDownloadBlocked) return; + + if (_serverConfigurationManager.IsUidBlacklisted(UserData.UID)) + HoldApplication("Blacklist", maxValue: 1); + + CachedPlayer.ApplyCharacterData(Guid.NewGuid(), RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, forced); + } + + public void CreateCachedPlayer(OnlineUserIdentDto? dto = null) + { + try + { + _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); + } + finally + { + _creationSemaphore.Release(); + } + } + + public string? GetNote() + { + return _serverConfigurationManager.GetNoteForUid(UserData.UID); + } + + public string? GetPlayerName() + { + if (CachedPlayer != null && CachedPlayer.PlayerName != null) + return CachedPlayer.PlayerName; + else + return _serverConfigurationManager.GetNameForUid(UserData.UID); + } + + public uint GetPlayerCharacterId() + { + if (CachedPlayer != null) + return CachedPlayer.PlayerCharacterId; + return uint.MaxValue; + } + + public string? GetNoteOrName() + { + string? note = GetNote(); + if (_mareConfig.Current.ShowCharacterNames || IsVisible) + return note ?? GetPlayerName(); + else + return note; + } + + public string GetPairSortKey() + { + string? noteOrName = GetNoteOrName(); + + if (noteOrName != null) + return $"0{noteOrName}"; + else + return $"9{UserData.AliasOrUID}"; + } + + public string GetPlayerNameHash() + { + return CachedPlayer?.PlayerNameHash ?? string.Empty; + } + + public bool HasAnyConnection() + { + return UserPair != null || GroupPair.Any(); + } + + 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(); + } + } + + public void SetNote(string note) + { + _serverConfigurationManager.SetNoteForUid(UserData.UID, note); + } + + internal void SetIsUploading() + { + CachedPlayer?.SetUploading(); + } + + public void HoldApplication(string source, int maxValue = int.MaxValue) + { + _logger.LogDebug($"Holding {UserData.UID} for reason: {source}"); + bool wasHeld = IsApplicationBlocked; + HoldApplicationLocks.AddOrUpdate(source, 1, (k, v) => Math.Min(maxValue, v + 1)); + if (!wasHeld) + CachedPlayer?.UndoApplication(); + } + + public void UnholdApplication(string source, bool skipApplication = false) + { + _logger.LogDebug($"Un-holding {UserData.UID} for reason: {source}"); + bool wasHeld = IsApplicationBlocked; + HoldApplicationLocks.AddOrUpdate(source, 0, (k, v) => Math.Max(0, v - 1)); + HoldApplicationLocks.TryRemove(new(source, 0)); + if (!skipApplication && wasHeld && !IsApplicationBlocked) + ApplyLastReceivedData(forced: true); + } + + public void HoldDownloads(string source, int maxValue = int.MaxValue) + { + _logger.LogDebug($"Holding {UserData.UID} for reason: {source}"); + bool wasHeld = IsApplicationBlocked; + HoldDownloadLocks.AddOrUpdate(source, 1, (k, v) => Math.Min(maxValue, v + 1)); + if (!wasHeld) + CachedPlayer?.UndoApplication(); + } + + public void UnholdDownloads(string source, bool skipApplication = false) + { + _logger.LogDebug($"Un-holding {UserData.UID} for reason: {source}"); + bool wasHeld = IsApplicationBlocked; + HoldDownloadLocks.AddOrUpdate(source, 0, (k, v) => Math.Max(0, v - 1)); + HoldDownloadLocks.TryRemove(new(source, 0)); + if (!skipApplication && wasHeld && !IsApplicationBlocked) + ApplyLastReceivedData(forced: true); + } + + private CharacterData? RemoveNotSyncedFiles(CharacterData? data) + { + _logger.LogTrace("Removing not synced files"); + if (data == null) + { + _logger.LogTrace("Nothing to remove"); + return data; + } + + var ActiveGroupPairs = GroupPair.Where(p => !p.Value.GroupUserPermissions.IsPaused() && !p.Key.GroupUserPermissions.IsPaused()).ToList(); + + bool disableIndividualAnimations = UserPair != null && (UserPair.OtherPermissions.IsDisableAnimations() || UserPair.OwnPermissions.IsDisableAnimations()); + bool disableIndividualVFX = UserPair != null && (UserPair.OtherPermissions.IsDisableVFX() || UserPair.OwnPermissions.IsDisableVFX()); + bool disableGroupAnimations = ActiveGroupPairs.All(pair => pair.Value.GroupUserPermissions.IsDisableAnimations() || pair.Key.GroupPermissions.IsDisableAnimations() || pair.Key.GroupUserPermissions.IsDisableAnimations()); + + bool disableAnimations = (UserPair != null && disableIndividualAnimations) || (UserPair == null && disableGroupAnimations); + + bool disableIndividualSounds = UserPair != null && (UserPair.OtherPermissions.IsDisableSounds() || UserPair.OwnPermissions.IsDisableSounds()); + bool disableGroupSounds = ActiveGroupPairs.All(pair => pair.Value.GroupUserPermissions.IsDisableSounds() || pair.Key.GroupPermissions.IsDisableSounds() || pair.Key.GroupUserPermissions.IsDisableSounds()); + bool disableGroupVFX = ActiveGroupPairs.All(pair => pair.Value.GroupUserPermissions.IsDisableVFX() || pair.Key.GroupPermissions.IsDisableVFX() || pair.Key.GroupUserPermissions.IsDisableVFX()); + + bool disableSounds = (UserPair != null && disableIndividualSounds) || (UserPair == null && disableGroupSounds); + bool disableVFX = (UserPair != null && disableIndividualVFX) || (UserPair == null && disableGroupVFX); + + _logger.LogTrace("Disable: Sounds: {disableSounds}, Anims: {disableAnimations}, VFX: {disableVFX}", + disableSounds, disableAnimations, disableVFX); + + if (disableAnimations || disableSounds || disableVFX) + { + _logger.LogTrace("Data cleaned up: Animations disabled: {disableAnimations}, Sounds disabled: {disableSounds}, VFX disabled: {disableVFX}", + disableAnimations, disableSounds, disableVFX); + foreach (var objectKind in data.FileReplacements.Select(k => k.Key)) + { + if (disableSounds) + data.FileReplacements[objectKind] = data.FileReplacements[objectKind] + .Where(f => !f.GamePaths.Any(p => p.EndsWith("scd", StringComparison.OrdinalIgnoreCase))) + .ToList(); + if (disableAnimations) + data.FileReplacements[objectKind] = data.FileReplacements[objectKind] + .Where(f => !f.GamePaths.Any(p => p.EndsWith("tmb", StringComparison.OrdinalIgnoreCase) || p.EndsWith("pap", StringComparison.OrdinalIgnoreCase))) + .ToList(); + if (disableVFX) + 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; + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Pairs/PairManager.cs b/MareSynchronos/PlayerData/Pairs/PairManager.cs new file mode 100644 index 0000000..1b5d89a --- /dev/null +++ b/MareSynchronos/PlayerData/Pairs/PairManager.cs @@ -0,0 +1,403 @@ +using Dalamud.Plugin.Services; +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Comparer; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.PlayerData.Factories; +using MareSynchronos.Services.Events; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace MareSynchronos.PlayerData.Pairs; + +public sealed class PairManager : DisposableMediatorSubscriberBase +{ + private readonly ConcurrentDictionary _allClientPairs = new(UserDataComparer.Instance); + private readonly ConcurrentDictionary _allGroups = new(GroupDataComparer.Instance); + private readonly MareConfigService _configurationService; + private readonly IContextMenu _dalamudContextMenu; + private readonly PairFactory _pairFactory; + private Lazy> _directPairsInternal; + private Lazy>> _groupPairsInternal; + + public PairManager(ILogger logger, PairFactory pairFactory, + MareConfigService configurationService, MareMediator mediator, + IContextMenu dalamudContextMenu) : base(logger, mediator) + { + _pairFactory = pairFactory; + _configurationService = configurationService; + _dalamudContextMenu = dalamudContextMenu; + Mediator.Subscribe(this, (_) => ClearPairs()); + Mediator.Subscribe(this, (_) => ReapplyPairData()); + _directPairsInternal = DirectPairsLazy(); + _groupPairsInternal = GroupPairsLazy(); + + _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 void AddGroup(GroupFullInfoDto dto) + { + _allGroups[dto.Group] = dto; + RecreateLazy(); + } + + public void AddGroupPair(GroupPairFullInfoDto dto) + { + if (!_allClientPairs.ContainsKey(dto.User)) + _allClientPairs[dto.User] = _pairFactory.Create(dto.User); + + var group = _allGroups[dto.Group]; + _allClientPairs[dto.User].GroupPair[group] = dto; + RecreateLazy(); + } + + public Pair? GetPairByUID(string uid) + { + var existingPair = _allClientPairs.FirstOrDefault(f => uid.Equals(f.Key.UID, StringComparison.Ordinal)); + if (!Equals(existingPair, default(KeyValuePair))) + { + return existingPair.Value; + } + + return null; + } + + public void AddUserPair(UserPairDto dto, bool addToLastAddedUser = true) + { + if (!_allClientPairs.ContainsKey(dto.User)) + { + _allClientPairs[dto.User] = _pairFactory.Create(dto.User); + } + else + { + addToLastAddedUser = false; + } + + _allClientPairs[dto.User].UserPair = dto; + if (addToLastAddedUser) + LastAddedUser = _allClientPairs[dto.User]; + _allClientPairs[dto.User].ApplyLastReceivedData(); + RecreateLazy(); + } + + public void ClearPairs() + { + Logger.LogDebug("Clearing all Pairs"); + 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).ToList(); + + public void MarkPairOffline(UserData user) + { + if (_allClientPairs.TryGetValue(user, out var pair)) + { + Mediator.Publish(new ClearProfileDataMessage(pair.UserData)); + pair.MarkOffline(); + } + + RecreateLazy(); + } + + public void MarkPairOnline(OnlineUserIdentDto dto, bool sendNotif = true) + { + if (!_allClientPairs.ContainsKey(dto.User)) throw new InvalidOperationException("No user found for " + dto); + + Mediator.Publish(new ClearProfileDataMessage(dto.User)); + + var pair = _allClientPairs[dto.User]; + if (pair.HasCachedPlayer) + { + RecreateLazy(); + return; + } + + if (sendNotif && _configurationService.Current.ShowOnlineNotifications + && (_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs && pair.UserPair != null + || !_configurationService.Current.ShowOnlineNotificationsOnlyForIndividualPairs) + && (_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs && !string.IsNullOrEmpty(pair.GetNote()) + || !_configurationService.Current.ShowOnlineNotificationsOnlyForNamedPairs)) + { + string? note = pair.GetNoteOrName(); + var msg = !string.IsNullOrEmpty(note) + ? $"{note} ({pair.UserData.AliasOrUID}) is now online" + : $"{pair.UserData.AliasOrUID} is now online"; + Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5))); + } + + pair.CreateCachedPlayer(dto); + + RecreateLazy(); + } + + public void ReceiveCharaData(OnlineUserCharaDataDto dto) + { + 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()) + { + foreach (var grpPair in item.Value.GroupPair.Select(k => k.Key).Where(grpPair => GroupDataComparer.Instance.Equals(grpPair.Group, data)).ToList()) + { + _allClientPairs[item.Key].GroupPair.Remove(grpPair); + } + + if (!_allClientPairs[item.Key].HasAnyConnection() && _allClientPairs.TryRemove(item.Key, out var pair)) + { + pair.MarkOffline(); + } + } + + RecreateLazy(); + } + + public void RemoveGroupPair(GroupPairDto dto) + { + if (_allClientPairs.TryGetValue(dto.User, out var pair)) + { + var group = _allGroups[dto.Group]; + pair.GroupPair.Remove(group); + + 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 = null; + + 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() + || pair.UserPair.OtherPermissions.IsPaired() != dto.Permissions.IsPaired()) + { + Mediator.Publish(new ClearProfileDataMessage(dto.User)); + } + + pair.UserPair.OtherPermissions = dto.Permissions; + + Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}", + pair.UserPair.OtherPermissions.IsPaused(), + pair.UserPair.OtherPermissions.IsDisableAnimations(), + pair.UserPair.OtherPermissions.IsDisableSounds(), + pair.UserPair.OtherPermissions.IsDisableVFX()); + + if (!pair.IsPaused) + pair.ApplyLastReceivedData(); + + RecreateLazy(); + } + + public void UpdateSelfPairPermissions(UserPermissionsDto dto) + { + if (!_allClientPairs.TryGetValue(dto.User, out var pair)) + { + throw new InvalidOperationException("No such pair for " + dto); + } + + if (pair.UserPair == null) throw new InvalidOperationException("No direct pair for " + dto); + + if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused() + || pair.UserPair.OwnPermissions.IsPaired() != dto.Permissions.IsPaired()) + { + Mediator.Publish(new ClearProfileDataMessage(dto.User)); + } + + pair.UserPair.OwnPermissions = dto.Permissions; + + Logger.LogTrace("Paused: {paused}, Anims: {anims}, Sounds: {sounds}, VFX: {vfx}", + pair.UserPair.OwnPermissions.IsPaused(), + pair.UserPair.OwnPermissions.IsDisableAnimations(), + pair.UserPair.OwnPermissions.IsDisableSounds(), + pair.UserPair.OwnPermissions.IsDisableVFX()); + + if (!pair.IsPaused) + pair.ApplyLastReceivedData(); + + RecreateLazy(); + } + + internal void ReceiveUploadStatus(UserDto dto) + { + if (_allClientPairs.TryGetValue(dto.User, out var existingPair) && existingPair.IsVisible) + { + existingPair.SetIsUploading(); + } + } + + internal void SetGroupPairStatusInfo(GroupPairUserInfoDto dto) + { + var group = _allGroups[dto.Group]; + _allClientPairs[dto.User].GroupPair[group].GroupPairStatusInfo = dto.GroupUserInfo; + RecreateLazy(); + } + + internal void SetGroupPairUserPermissions(GroupPairUserPermissionDto dto) + { + var group = _allGroups[dto.Group]; + var prevPermissions = _allClientPairs[dto.User].GroupPair[group].GroupUserPermissions; + _allClientPairs[dto.User].GroupPair[group].GroupUserPermissions = dto.GroupPairPermissions; + if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations() + || prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds() + || prevPermissions.IsDisableVFX() != dto.GroupPairPermissions.IsDisableVFX()) + { + _allClientPairs[dto.User].ApplyLastReceivedData(); + } + RecreateLazy(); + } + + internal void SetGroupPermissions(GroupPermissionDto dto) + { + var prevPermissions = _allGroups[dto.Group].GroupPermissions; + _allGroups[dto.Group].GroupPermissions = dto.Permissions; + if (prevPermissions.IsDisableAnimations() != dto.Permissions.IsDisableAnimations() + || prevPermissions.IsDisableSounds() != dto.Permissions.IsDisableSounds() + || prevPermissions.IsDisableVFX() != dto.Permissions.IsDisableVFX()) + { + RecreateLazy(); + var group = _allGroups[dto.Group]; + GroupPairs[group].ForEach(p => p.ApplyLastReceivedData()); + } + RecreateLazy(); + } + + internal void SetGroupStatusInfo(GroupPairUserInfoDto dto) + { + _allGroups[dto.Group].GroupUserInfo = dto.GroupUserInfo; + RecreateLazy(); + } + + internal void SetGroupUserPermissions(GroupPairUserPermissionDto dto) + { + var prevPermissions = _allGroups[dto.Group].GroupUserPermissions; + _allGroups[dto.Group].GroupUserPermissions = dto.GroupPairPermissions; + if (prevPermissions.IsDisableAnimations() != dto.GroupPairPermissions.IsDisableAnimations() + || prevPermissions.IsDisableSounds() != dto.GroupPairPermissions.IsDisableSounds() + || prevPermissions.IsDisableVFX() != dto.GroupPairPermissions.IsDisableVFX()) + { + RecreateLazy(); + var group = _allGroups[dto.Group]; + GroupPairs[group].ForEach(p => p.ApplyLastReceivedData()); + } + RecreateLazy(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu; + + DisposePairs(); + } + + private void DalamudContextMenuOnOnOpenGameObjectContextMenu(Dalamud.Game.Gui.ContextMenu.IMenuOpenedArgs args) + { + if (args.MenuType == Dalamud.Game.Gui.ContextMenu.ContextMenuType.Inventory) return; + if (!_configurationService.Current.EnableRightClickMenus) return; + + foreach (var pair in _allClientPairs.Where((p => p.Value.IsVisible))) + { + pair.Value.AddContextMenu(args); + } + } + + private Lazy> DirectPairsLazy() => new(() => _allClientPairs.Select(k => k.Value) + .Where(k => k.UserPair != null).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 = new(); + foreach (var group in _allGroups) + { + outDict[group.Value] = _allClientPairs.Select(p => p.Value).Where(p => p.GroupPair.Any(g => GroupDataComparer.Instance.Equals(group.Key, g.Key.Group))).ToList(); + } + return outDict; + }); + } + + private void ReapplyPairData() + { + foreach (var pair in _allClientPairs.Select(k => k.Value)) + { + pair.ApplyLastReceivedData(forced: true); + } + } + + private void RecreateLazy() + { + _directPairsInternal = DirectPairsLazy(); + _groupPairsInternal = GroupPairsLazy(); + } +} \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Services/CacheCreationService.cs b/MareSynchronos/PlayerData/Services/CacheCreationService.cs new file mode 100644 index 0000000..a3603c2 --- /dev/null +++ b/MareSynchronos/PlayerData/Services/CacheCreationService.cs @@ -0,0 +1,263 @@ +using MareSynchronos.API.Data.Enum; +using MareSynchronos.PlayerData.Data; +using MareSynchronos.PlayerData.Factories; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.PlayerData.Services; + +#pragma warning disable MA0040 + +public sealed class CacheCreationService : DisposableMediatorSubscriberBase +{ + private readonly SemaphoreSlim _cacheCreateLock = new(1); + private readonly Dictionary _cachesToCreate = []; + private readonly PlayerDataFactory _characterDataFactory; + private readonly CancellationTokenSource _cts = new(); + private readonly CharacterData _playerData = new(); + private readonly Dictionary _playerRelatedObjects = []; + private Task? _cacheCreationTask; + private CancellationTokenSource _honorificCts = new(); + private CancellationTokenSource _petNicknamesCts = new(); + private CancellationTokenSource _moodlesCts = new(); + private bool _isZoning = false; + private bool _haltCharaDataCreation; + private readonly Dictionary _glamourerCts = new(); + + public CacheCreationService(ILogger logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory, + PlayerDataFactory characterDataFactory, DalamudUtilService dalamudUtil) : base(logger, mediator) + { + _characterDataFactory = characterDataFactory; + + Mediator.Subscribe(this, (msg) => + { + Logger.LogDebug("Received CreateCacheForObject for {handler}, updating", msg.ObjectToCreateFor); + _cacheCreateLock.Wait(); + _cachesToCreate[msg.ObjectToCreateFor.ObjectKind] = msg.ObjectToCreateFor; + _cacheCreateLock.Release(); + }); + + Mediator.Subscribe(this, (msg) => _isZoning = true); + Mediator.Subscribe(this, (msg) => _isZoning = false); + + Mediator.Subscribe(this, (msg) => + { + _haltCharaDataCreation = !msg.Resume; + }); + + _playerRelatedObjects[ObjectKind.Player] = gameObjectHandlerFactory.Create(ObjectKind.Player, dalamudUtil.GetPlayerPointer, isWatched: true) + .GetAwaiter().GetResult(); + _playerRelatedObjects[ObjectKind.MinionOrMount] = gameObjectHandlerFactory.Create(ObjectKind.MinionOrMount, () => dalamudUtil.GetMinionOrMount(), isWatched: true) + .GetAwaiter().GetResult(); + _playerRelatedObjects[ObjectKind.Pet] = gameObjectHandlerFactory.Create(ObjectKind.Pet, () => dalamudUtil.GetPet(), isWatched: true) + .GetAwaiter().GetResult(); + _playerRelatedObjects[ObjectKind.Companion] = gameObjectHandlerFactory.Create(ObjectKind.Companion, () => dalamudUtil.GetCompanion(), isWatched: true) + .GetAwaiter().GetResult(); + + Mediator.Subscribe(this, (msg) => + { + if (msg.GameObjectHandler != _playerRelatedObjects[ObjectKind.Player]) return; + + Logger.LogTrace("Removing pet data for {obj}", msg.GameObjectHandler); + _playerData.FileReplacements.Remove(ObjectKind.Pet); + _playerData.GlamourerString.Remove(ObjectKind.Pet); + _playerData.CustomizePlusScale.Remove(ObjectKind.Pet); + Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI())); + }); + + Mediator.Subscribe(this, (msg) => + { + // ignore pets + if (msg.ObjectToCreateFor == _playerRelatedObjects[ObjectKind.Pet]) return; + _ = Task.Run(() => + { + Logger.LogTrace("Clearing cache for {obj}", msg.ObjectToCreateFor); + _playerData.FileReplacements.Remove(msg.ObjectToCreateFor.ObjectKind); + _playerData.GlamourerString.Remove(msg.ObjectToCreateFor.ObjectKind); + _playerData.CustomizePlusScale.Remove(msg.ObjectToCreateFor.ObjectKind); + Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI())); + }); + }); + + Mediator.Subscribe(this, (msg) => + { + if (_isZoning) return; + _ = Task.Run(async () => + { + + foreach (var item in _playerRelatedObjects + .Where(item => msg.Address == null + || item.Value.Address == msg.Address).Select(k => k.Key)) + { + Logger.LogDebug("Received CustomizePlus change, updating {obj}", item); + await AddPlayerCacheToCreate(item).ConfigureAwait(false); + } + }); + }); + Mediator.Subscribe(this, (msg) => + { + if (_isZoning) return; + Logger.LogDebug("Received Heels Offset change, updating player"); + _ = AddPlayerCacheToCreate(); + }); + Mediator.Subscribe(this, (msg) => + { + if (_isZoning) return; + var changedType = _playerRelatedObjects.FirstOrDefault(f => f.Value.Address == msg.Address); + if (changedType.Key != default || changedType.Value != default) + { + GlamourerChanged(changedType.Key); + } + }); + Mediator.Subscribe(this, (msg) => + { + if (_isZoning) return; + if (!string.Equals(msg.NewHonorificTitle, _playerData.HonorificData, StringComparison.Ordinal)) + { + Logger.LogDebug("Received Honorific change, updating player"); + HonorificChanged(); + } + }); + Mediator.Subscribe(this, (msg) => + { + if (_isZoning) return; + if (!string.Equals(msg.PetNicknamesData, _playerData.PetNamesData, StringComparison.Ordinal)) + { + Logger.LogDebug("Received Pet Nicknames change, updating player"); + PetNicknamesChanged(); + } + }); + Mediator.Subscribe(this, (msg) => + { + if (_isZoning) return; + var changedType = _playerRelatedObjects.FirstOrDefault(f => f.Value.Address == msg.Address); + if (changedType.Key == ObjectKind.Player && changedType.Value != default) + { + Logger.LogDebug("Received Moodles change, updating player"); + MoodlesChanged(); + } + }); + Mediator.Subscribe(this, (msg) => + { + Logger.LogDebug("Received Penumbra Mod settings change, updating player"); + AddPlayerCacheToCreate().GetAwaiter().GetResult(); + }); + + Mediator.Subscribe(this, (msg) => ProcessCacheCreation()); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _playerRelatedObjects.Values.ToList().ForEach(p => p.Dispose()); + _cts.Dispose(); + } + + private async Task AddPlayerCacheToCreate(ObjectKind kind = ObjectKind.Player) + { + await _cacheCreateLock.WaitAsync().ConfigureAwait(false); + _cachesToCreate[kind] = _playerRelatedObjects[kind]; + _cacheCreateLock.Release(); + } + + private void GlamourerChanged(ObjectKind kind) + { + if (_glamourerCts.TryGetValue(kind, out var cts)) + { + _glamourerCts[kind]?.Cancel(); + _glamourerCts[kind]?.Dispose(); + } + _glamourerCts[kind] = new(); + var token = _glamourerCts[kind].Token; + + _ = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMilliseconds(500), token).ConfigureAwait(false); + await AddPlayerCacheToCreate(kind).ConfigureAwait(false); + }); + } + + private void HonorificChanged() + { + _honorificCts?.Cancel(); + _honorificCts?.Dispose(); + _honorificCts = new(); + var token = _honorificCts.Token; + + _ = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); + await AddPlayerCacheToCreate().ConfigureAwait(false); + }, token); + } + + private void PetNicknamesChanged() + { + _petNicknamesCts?.Cancel(); + _petNicknamesCts?.Dispose(); + _petNicknamesCts = new(); + var token = _petNicknamesCts.Token; + + _ = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); + await AddPlayerCacheToCreate().ConfigureAwait(false); + }, token); + } + + private void MoodlesChanged() + { + _moodlesCts?.Cancel(); + _moodlesCts?.Dispose(); + _moodlesCts = new(); + var token = _moodlesCts.Token; + + _ = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); + await AddPlayerCacheToCreate().ConfigureAwait(false); + }, token); + } + + private void ProcessCacheCreation() + { + if (_isZoning || _haltCharaDataCreation) return; + + if (_cachesToCreate.Any() && (_cacheCreationTask?.IsCompleted ?? true)) + { + _cacheCreateLock.Wait(); + var toCreate = _cachesToCreate.ToList(); + _cachesToCreate.Clear(); + _cacheCreateLock.Release(); + + _cacheCreationTask = Task.Run(async () => + { + try + { + foreach (var obj in toCreate) + { + await _characterDataFactory.BuildCharacterData(_playerData, obj.Value, _cts.Token).ConfigureAwait(false); + } + + Mediator.Publish(new CharacterDataCreatedMessage(_playerData.ToAPI())); + } + catch (Exception ex) + { + Logger.LogCritical(ex, "Error during Cache Creation Processing"); + } + finally + { + Logger.LogDebug("Cache Creation complete"); + } + }, _cts.Token); + } + else if (_cachesToCreate.Any()) + { + Logger.LogDebug("Cache Creation stored until previous creation finished"); + } + } +} +#pragma warning restore MA0040 \ No newline at end of file diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs new file mode 100644 index 0000000..a2486a1 --- /dev/null +++ b/MareSynchronos/Plugin.cs @@ -0,0 +1,230 @@ +using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using MareSynchronos.FileCache; +using MareSynchronos.Interop; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Configurations; +using MareSynchronos.PlayerData.Factories; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.PlayerData.Services; +using MareSynchronos.Services; +using MareSynchronos.Services.Events; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI; +using MareSynchronos.UI.Components; +using MareSynchronos.UI.Components.Popup; +using MareSynchronos.UI.Handlers; +using MareSynchronos.WebAPI; +using MareSynchronos.WebAPI.Files; +using MareSynchronos.WebAPI.SignalR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MareSynchronos.Services.CharaData; + +using MareSynchronos; + +namespace Snowcloak; + +public sealed class Plugin : IDalamudPlugin +{ + private readonly IHost _host; + +#pragma warning disable CA2211, CS8618, MA0069, S1104, S2223 + public static Plugin Self; +#pragma warning restore CA2211, CS8618, MA0069, S1104, S2223 + public Action? RealOnFrameworkUpdate { get; set; } + + // Proxy function in the SnowcloakSync namespace to avoid confusion in /xlstats + public void OnFrameworkUpdate(IFramework framework) + { + RealOnFrameworkUpdate?.Invoke(framework); + } + + public Plugin(IDalamudPluginInterface pluginInterface, ICommandManager commandManager, IDataManager gameData, + IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui, + IGameGui gameGui, IDtrBar dtrBar, IToastGui toastGui, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager, + ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, + INamePlateGui namePlateGui, IGameConfig gameConfig, IPartyList partyList) + { + Plugin.Self = this; + _host = new HostBuilder() + .UseContentRoot(pluginInterface.ConfigDirectory.FullName) + .ConfigureLogging(lb => + { + lb.ClearProviders(); + lb.AddDalamudLogging(pluginLog); + lb.SetMinimumLevel(LogLevel.Trace); + }) + .ConfigureServices(collection => + { + collection.AddSingleton(new WindowSystem("MareSynchronos")); + collection.AddSingleton(); + + // add dalamud services + collection.AddSingleton(_ => pluginInterface); + collection.AddSingleton(_ => pluginInterface.UiBuilder); + collection.AddSingleton(_ => commandManager); + collection.AddSingleton(_ => gameData); + collection.AddSingleton(_ => framework); + collection.AddSingleton(_ => objectTable); + collection.AddSingleton(_ => clientState); + collection.AddSingleton(_ => condition); + collection.AddSingleton(_ => chatGui); + collection.AddSingleton(_ => gameGui); + collection.AddSingleton(_ => dtrBar); + collection.AddSingleton(_ => toastGui); + collection.AddSingleton(_ => pluginLog); + collection.AddSingleton(_ => targetManager); + collection.AddSingleton(_ => notificationManager); + collection.AddSingleton(_ => textureProvider); + collection.AddSingleton(_ => contextMenu); + collection.AddSingleton(_ => gameInteropProvider); + collection.AddSingleton(_ => namePlateGui); + collection.AddSingleton(_ => gameConfig); + collection.AddSingleton(_ => partyList); + + // add mare 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(); + 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(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); + + collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new NotesConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new ServerTagConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new SyncshellConfigService(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 ServerBlockConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName)); + collection.AddSingleton((s) => new RemoteConfigCacheService(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>(s => s.GetRequiredService()); + collection.AddSingleton(); + collection.AddSingleton(); + + collection.AddSingleton(); + + // 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(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.AddScoped(); + collection.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()); + }) + .Build(); + + _ = Task.Run(async () => { + try + { + await _host.StartAsync().ConfigureAwait(false); + } + catch (Exception e) + { + pluginLog.Error(e, "HostBuilder startup exception"); + } + }).ConfigureAwait(false); + } + + public void Dispose() + { + _host.StopAsync().GetAwaiter().GetResult(); + _host.Dispose(); + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs b/MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs new file mode 100644 index 0000000..2b76ddf --- /dev/null +++ b/MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs @@ -0,0 +1,133 @@ +using MareSynchronos.API.Data.Enum; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.PlayerData.Factories; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services.CharaData.Models; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase +{ + private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; + private readonly DalamudUtilService _dalamudUtilService; + private readonly IpcManager _ipcManager; + private readonly Dictionary _handledCharaData = new(StringComparer.Ordinal); + + public IReadOnlyDictionary HandledCharaData => _handledCharaData; + + public CharaDataCharacterHandler(ILogger logger, MareMediator mediator, + GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService, + IpcManager ipcManager) + : base(logger, mediator) + { + _gameObjectHandlerFactory = gameObjectHandlerFactory; + _dalamudUtilService = dalamudUtilService; + _ipcManager = ipcManager; + mediator.Subscribe(this, msg => + { + foreach (var chara in _handledCharaData) + { + _ = RevertHandledChara(chara.Value); + } + }); + + mediator.Subscribe(this, (_) => HandleCutsceneFrameworkUpdate()); + } + + private void HandleCutsceneFrameworkUpdate() + { + if (!_dalamudUtilService.IsInGpose) return; + + foreach (var entry in _handledCharaData.Values.ToList()) + { + var chara = _dalamudUtilService.GetGposeCharacterFromObjectTableByName(entry.Name, onlyGposeCharacters: true); + if (chara is null) + { + _handledCharaData.Remove(entry.Name); + _ = _dalamudUtilService.RunOnFrameworkThread(() => RevertChara(entry.Name, entry.CustomizePlus)); + } + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + foreach (var chara in _handledCharaData.Values) + { + _ = RevertHandledChara(chara); + } + } + + public HandledCharaDataEntry? GetHandledCharacter(string name) + { + return _handledCharaData.GetValueOrDefault(name); + } + + public async Task RevertChara(string name, Guid? cPlusId) + { + Guid applicationId = Guid.NewGuid(); + await _ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).ConfigureAwait(false); + if (cPlusId != null) + { + await _ipcManager.CustomizePlus.RevertByIdAsync(cPlusId).ConfigureAwait(false); + } + using var handler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, + () => _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, _dalamudUtilService.IsInGpose)?.Address ?? IntPtr.Zero, false) + .ConfigureAwait(false); + if (handler.Address != nint.Zero) + await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, CancellationToken.None).ConfigureAwait(false); + } + + public async Task RevertHandledChara(string name) + { + var handled = _handledCharaData.GetValueOrDefault(name); + return await RevertHandledChara(handled).ConfigureAwait(false); + } + + public async Task RevertHandledChara(HandledCharaDataEntry? handled) + { + if (handled == null) return false; + _handledCharaData.Remove(handled.Name); + await _dalamudUtilService.RunOnFrameworkThread(async () => + { + await RevertChara(handled.Name, handled.CustomizePlus).ConfigureAwait(false); + }).ConfigureAwait(false); + return true; + } + + internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry) + { + _handledCharaData.Add(handledCharaDataEntry.Name, handledCharaDataEntry); + } + + public void UpdateHandledData(Dictionary newData) + { + foreach (var handledData in _handledCharaData.Values) + { + if (newData.TryGetValue(handledData.MetaInfo.FullId, out var metaInfo) && metaInfo != null) + { + handledData.MetaInfo = metaInfo; + } + } + } + + public async Task TryCreateGameObjectHandler(string name, bool gPoseOnly = false) + { + var handler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, + () => _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, gPoseOnly && _dalamudUtilService.IsInGpose)?.Address ?? IntPtr.Zero, false) + .ConfigureAwait(false); + if (handler.Address == nint.Zero) return null; + return handler; + } + + public async Task TryCreateGameObjectHandler(int index) + { + var handler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, + () => _dalamudUtilService.GetCharacterFromObjectTableByIndex(index)?.Address ?? IntPtr.Zero, false) + .ConfigureAwait(false); + if (handler.Address == nint.Zero) return null; + return handler; + } +} diff --git a/MareSynchronos/Services/CharaData/CharaDataFileHandler.cs b/MareSynchronos/Services/CharaData/CharaDataFileHandler.cs new file mode 100644 index 0000000..6bb1297 --- /dev/null +++ b/MareSynchronos/Services/CharaData/CharaDataFileHandler.cs @@ -0,0 +1,302 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using K4os.Compression.LZ4.Legacy; +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.FileCache; +using MareSynchronos.PlayerData.Factories; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services.CharaData; +using MareSynchronos.Services.CharaData.Models; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI.Files; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public sealed class CharaDataFileHandler : IDisposable +{ + private readonly DalamudUtilService _dalamudUtilService; + private readonly FileCacheManager _fileCacheManager; + private readonly FileDownloadManager _fileDownloadManager; + private readonly FileUploadManager _fileUploadManager; + private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; + private readonly ILogger _logger; + private readonly MareCharaFileDataFactory _mareCharaFileDataFactory; + private readonly PlayerDataFactory _playerDataFactory; + private int _globalFileCounter = 0; + + public CharaDataFileHandler(ILogger logger, FileDownloadManagerFactory fileDownloadManagerFactory, FileUploadManager fileUploadManager, FileCacheManager fileCacheManager, + DalamudUtilService dalamudUtilService, GameObjectHandlerFactory gameObjectHandlerFactory, PlayerDataFactory playerDataFactory) + { + _fileDownloadManager = fileDownloadManagerFactory.Create(); + _logger = logger; + _fileUploadManager = fileUploadManager; + _fileCacheManager = fileCacheManager; + _dalamudUtilService = dalamudUtilService; + _gameObjectHandlerFactory = gameObjectHandlerFactory; + _playerDataFactory = playerDataFactory; + _mareCharaFileDataFactory = new(fileCacheManager); + } + + public void ComputeMissingFiles(CharaDataDownloadDto charaDataDownloadDto, out Dictionary modPaths, out List missingFiles) + { + modPaths = []; + missingFiles = []; + foreach (var file in charaDataDownloadDto.FileGamePaths) + { + var localCacheFile = _fileCacheManager.GetFileCacheByHash(file.HashOrFileSwap); + if (localCacheFile == null) + { + var existingFile = missingFiles.Find(f => string.Equals(f.Hash, file.HashOrFileSwap, StringComparison.Ordinal)); + if (existingFile == null) + { + missingFiles.Add(new FileReplacementData() + { + Hash = file.HashOrFileSwap, + GamePaths = [file.GamePath] + }); + } + else + { + existingFile.GamePaths = existingFile.GamePaths.Concat([file.GamePath]).ToArray(); + } + } + else + { + modPaths[file.GamePath] = localCacheFile.ResolvedFilepath; + } + } + + foreach (var swap in charaDataDownloadDto.FileSwaps) + { + modPaths[swap.GamePath] = swap.HashOrFileSwap; + } + } + + public async Task CreatePlayerData() + { + var chara = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false); + if (_dalamudUtilService.IsInGpose) + { + chara = (IPlayerCharacter?)(await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(chara.Name.TextValue, _dalamudUtilService.IsInGpose).ConfigureAwait(false)); + } + + if (chara == null) + return null; + + using var tempHandler = await _gameObjectHandlerFactory.Create(ObjectKind.Player, + () => _dalamudUtilService.GetCharacterFromObjectTableByIndex(chara.ObjectIndex)?.Address ?? IntPtr.Zero, isWatched: false).ConfigureAwait(false); + PlayerData.Data.CharacterData newCdata = new(); + await _playerDataFactory.BuildCharacterData(newCdata, tempHandler, CancellationToken.None).ConfigureAwait(false); + if (newCdata.FileReplacements.TryGetValue(ObjectKind.Player, out var playerData) && playerData != null) + { + foreach (var data in playerData.Select(g => g.GamePaths)) + { + data.RemoveWhere(g => g.EndsWith(".pap", StringComparison.OrdinalIgnoreCase) + || g.EndsWith(".tmb", StringComparison.OrdinalIgnoreCase) + || g.EndsWith(".scd", StringComparison.OrdinalIgnoreCase) + || (g.EndsWith(".avfx", StringComparison.OrdinalIgnoreCase) + && !g.Contains("/weapon/", StringComparison.OrdinalIgnoreCase) + && !g.Contains("/equipment/", StringComparison.OrdinalIgnoreCase)) + || (g.EndsWith(".atex", StringComparison.OrdinalIgnoreCase) + && !g.Contains("/weapon/", StringComparison.OrdinalIgnoreCase) + && !g.Contains("/equipment/", StringComparison.OrdinalIgnoreCase))); + } + + playerData.RemoveWhere(g => g.GamePaths.Count == 0); + } + + return newCdata.ToAPI(); + } + + public void Dispose() + { + _fileDownloadManager.Dispose(); + } + + public async Task DownloadFilesAsync(GameObjectHandler tempHandler, List missingFiles, Dictionary modPaths, CancellationToken token) + { + await _fileDownloadManager.InitiateDownloadList(tempHandler, missingFiles, token).ConfigureAwait(false); + await _fileDownloadManager.DownloadFiles(tempHandler, missingFiles, token).ConfigureAwait(false); + token.ThrowIfCancellationRequested(); + foreach (var file in missingFiles.SelectMany(m => m.GamePaths, (FileEntry, GamePath) => (FileEntry.Hash, GamePath))) + { + var localFile = _fileCacheManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath; + if (localFile == null) + { + throw new FileNotFoundException("File not found locally."); + } + modPaths[file.GamePath] = localFile; + } + } + + public Task<(MareCharaFileHeader loadedCharaFile, long expectedLength)> LoadCharaFileHeader(string filePath) + { + try + { + using var unwrapped = File.OpenRead(filePath); + using var lz4Stream = new LZ4Stream(unwrapped, LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression); + using var reader = new BinaryReader(lz4Stream); + var loadedCharaFile = MareCharaFileHeader.FromBinaryReader(filePath, reader); + + _logger.LogInformation("Read Mare Chara File"); + _logger.LogInformation("Version: {ver}", (loadedCharaFile?.Version ?? -1)); + long expectedLength = 0; + if (loadedCharaFile != null) + { + _logger.LogTrace("Data"); + foreach (var item in loadedCharaFile.CharaFileData.FileSwaps) + { + foreach (var gamePath in item.GamePaths) + { + _logger.LogTrace("Swap: {gamePath} => {fileSwapPath}", gamePath, item.FileSwapPath); + } + } + + var itemNr = 0; + foreach (var item in loadedCharaFile.CharaFileData.Files) + { + itemNr++; + expectedLength += item.Length; + foreach (var gamePath in item.GamePaths) + { + _logger.LogTrace("File {itemNr}: {gamePath} = {len}", itemNr, gamePath, item.Length.ToByteString()); + } + } + + _logger.LogInformation("Expected length: {expected}", expectedLength.ToByteString()); + } + else + { + throw new InvalidOperationException("MCDF Header was null"); + } + return Task.FromResult((loadedCharaFile, expectedLength)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not parse MCDF header of file {file}", filePath); + throw; + } + } + + public Dictionary McdfExtractFiles(MareCharaFileHeader? charaFileHeader, long expectedLength, List extractedFiles) + { + if (charaFileHeader == null) return []; + + using var lz4Stream = new LZ4Stream(File.OpenRead(charaFileHeader.FilePath), LZ4StreamMode.Decompress, LZ4StreamFlags.HighCompression); + using var reader = new BinaryReader(lz4Stream); + MareCharaFileHeader.AdvanceReaderToData(reader); + + long totalRead = 0; + Dictionary gamePathToFilePath = new(StringComparer.Ordinal); + foreach (var fileData in charaFileHeader.CharaFileData.Files) + { + var fileName = Path.Combine(_fileCacheManager.CacheFolder, "mare_" + _globalFileCounter++ + ".tmp"); + extractedFiles.Add(fileName); + var length = fileData.Length; + var bufferSize = length; + using var fs = File.OpenWrite(fileName); + using var wr = new BinaryWriter(fs); + _logger.LogTrace("Reading {length} of {fileName}", length.ToByteString(), fileName); + var buffer = reader.ReadBytes(bufferSize); + wr.Write(buffer); + wr.Flush(); + wr.Close(); + if (buffer.Length == 0) throw new EndOfStreamException("Unexpected EOF"); + foreach (var path in fileData.GamePaths) + { + gamePathToFilePath[path] = fileName; + _logger.LogTrace("{path} => {fileName} [{hash}]", path, fileName, fileData.Hash); + } + totalRead += length; + _logger.LogTrace("Read {read}/{expected} bytes", totalRead.ToByteString(), expectedLength.ToByteString()); + } + + return gamePathToFilePath; + } + + public async Task UpdateCharaDataAsync(CharaDataExtendedUpdateDto updateDto) + { + var data = await CreatePlayerData().ConfigureAwait(false); + + if (data != null) + { + var hasGlamourerData = data.GlamourerData.TryGetValue(ObjectKind.Player, out var playerDataString); + if (!hasGlamourerData) updateDto.GlamourerData = null; + else updateDto.GlamourerData = playerDataString; + + var hasCustomizeData = data.CustomizePlusData.TryGetValue(ObjectKind.Player, out var customizeDataString); + if (!hasCustomizeData) updateDto.CustomizeData = null; + else updateDto.CustomizeData = customizeDataString; + + updateDto.ManipulationData = data.ManipulationData; + + var hasFiles = data.FileReplacements.TryGetValue(ObjectKind.Player, out var fileReplacements); + if (!hasFiles) + { + updateDto.FileGamePaths = []; + updateDto.FileSwaps = []; + } + else + { + updateDto.FileGamePaths = [.. fileReplacements!.Where(u => string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.Hash, path))]; + updateDto.FileSwaps = [.. fileReplacements!.Where(u => !string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.FileSwapPath, path))]; + } + } + } + + internal async Task SaveCharaFileAsync(string description, string filePath) + { + var tempFilePath = filePath + ".tmp"; + + try + { + var data = await CreatePlayerData().ConfigureAwait(false); + if (data == null) return; + + var mareCharaFileData = _mareCharaFileDataFactory.Create(description, data); + MareCharaFileHeader output = new(MareCharaFileHeader.CurrentVersion, mareCharaFileData); + + using var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); + using var lz4 = new LZ4Stream(fs, LZ4StreamMode.Compress, LZ4StreamFlags.HighCompression); + using var writer = new BinaryWriter(lz4); + output.WriteToStream(writer); + + foreach (var item in output.CharaFileData.Files) + { + var file = _fileCacheManager.GetFileCacheByHash(item.Hash)!; + _logger.LogDebug("Saving to MCDF: {hash}:{file}", item.Hash, file.ResolvedFilepath); + _logger.LogDebug("\tAssociated GamePaths:"); + foreach (var path in item.GamePaths) + { + _logger.LogDebug("\t{path}", path); + } + + var fsRead = File.OpenRead(file.ResolvedFilepath); + await using (fsRead.ConfigureAwait(false)) + { + using var br = new BinaryReader(fsRead); + byte[] buffer = new byte[item.Length]; + br.Read(buffer, 0, item.Length); + writer.Write(buffer); + } + } + writer.Flush(); + await lz4.FlushAsync().ConfigureAwait(false); + await fs.FlushAsync().ConfigureAwait(false); + fs.Close(); + File.Move(tempFilePath, filePath, true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failure Saving Mare Chara File, deleting output"); + File.Delete(tempFilePath); + } + } + + internal async Task> UploadFiles(List fileList, ValueProgress uploadProgress, CancellationToken token) + { + return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false); + } +} diff --git a/MareSynchronos/Services/CharaData/CharaDataGposeTogetherManager.cs b/MareSynchronos/Services/CharaData/CharaDataGposeTogetherManager.cs new file mode 100644 index 0000000..9eaad31 --- /dev/null +++ b/MareSynchronos/Services/CharaData/CharaDataGposeTogetherManager.cs @@ -0,0 +1,696 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.Interop; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.Services.CharaData.Models; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; +using System.Globalization; +using System.Numerics; +using System.Text.Json.Nodes; + +namespace MareSynchronos.Services.CharaData; + +public class CharaDataGposeTogetherManager : DisposableMediatorSubscriberBase +{ + private readonly ApiController _apiController; + private readonly IpcCallerBrio _brio; + private readonly SemaphoreSlim _charaDataCreationSemaphore = new(1, 1); + private readonly CharaDataFileHandler _charaDataFileHandler; + private readonly CharaDataManager _charaDataManager; + private readonly DalamudUtilService _dalamudUtil; + private readonly Dictionary _usersInLobby = []; + private readonly VfxSpawnManager _vfxSpawnManager; + private (CharacterData ApiData, CharaDataDownloadDto Dto)? _lastCreatedCharaData; + private PoseData? _lastDeltaPoseData; + private PoseData? _lastFullPoseData; + private WorldData? _lastWorldData; + private CancellationTokenSource _lobbyCts = new(); + private int _poseGenerationExecutions = 0; + + public CharaDataGposeTogetherManager(ILogger logger, MareMediator mediator, + ApiController apiController, IpcCallerBrio brio, DalamudUtilService dalamudUtil, VfxSpawnManager vfxSpawnManager, + CharaDataFileHandler charaDataFileHandler, CharaDataManager charaDataManager) : base(logger, mediator) + { + Mediator.Subscribe(this, (msg) => + { + OnUserJoinLobby(msg.UserData); + }); + Mediator.Subscribe(this, (msg) => + { + OnUserLeaveLobby(msg.UserData); + }); + Mediator.Subscribe(this, (msg) => + { + OnReceiveCharaData(msg.CharaDataDownloadDto); + }); + Mediator.Subscribe(this, (msg) => + { + OnReceivePoseData(msg.UserData, msg.PoseData); + }); + Mediator.Subscribe(this, (msg) => + { + OnReceiveWorldData(msg.UserData, msg.WorldData); + }); + Mediator.Subscribe(this, (msg) => + { + if (_usersInLobby.Count > 0 && !string.IsNullOrEmpty(CurrentGPoseLobbyId)) + { + JoinGPoseLobby(CurrentGPoseLobbyId, isReconnecting: true); + } + else + { + LeaveGPoseLobby(); + } + }); + Mediator.Subscribe(this, (msg) => + { + OnEnterGpose(); + }); + Mediator.Subscribe(this, (msg) => + { + OnExitGpose(); + }); + Mediator.Subscribe(this, (msg) => + { + OnFrameworkUpdate(); + }); + Mediator.Subscribe(this, (msg) => + { + OnCutsceneFrameworkUpdate(); + }); + Mediator.Subscribe(this, (msg) => + { + LeaveGPoseLobby(); + }); + + _apiController = apiController; + _brio = brio; + _dalamudUtil = dalamudUtil; + _vfxSpawnManager = vfxSpawnManager; + _charaDataFileHandler = charaDataFileHandler; + _charaDataManager = charaDataManager; + } + + public string? CurrentGPoseLobbyId { get; private set; } + public string? LastGPoseLobbyId { get; private set; } + + public IEnumerable UsersInLobby => _usersInLobby.Values; + + public (bool SameMap, bool SameServer, bool SameEverything) IsOnSameMapAndServer(GposeLobbyUserData data) + { + return (data.Map.RowId == _lastWorldData?.LocationInfo.MapId, data.WorldData?.LocationInfo.ServerId == _lastWorldData?.LocationInfo.ServerId, data.WorldData?.LocationInfo == _lastWorldData?.LocationInfo); + } + + public async Task PushCharacterDownloadDto() + { + var playerData = await _charaDataFileHandler.CreatePlayerData().ConfigureAwait(false); + if (playerData == null) return; + if (!string.Equals(playerData.DataHash.Value, _lastCreatedCharaData?.ApiData.DataHash.Value, StringComparison.Ordinal)) + { + List filegamePaths = [.. playerData.FileReplacements[API.Data.Enum.ObjectKind.Player] + .Where(u => string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.Hash, path))]; + List fileSwapPaths = [.. playerData.FileReplacements[API.Data.Enum.ObjectKind.Player] + .Where(u => !string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.FileSwapPath, path))]; + await _charaDataManager.UploadFiles([.. playerData.FileReplacements[API.Data.Enum.ObjectKind.Player] + .Where(u => string.IsNullOrEmpty(u.FileSwapPath)).SelectMany(u => u.GamePaths, (file, path) => new GamePathEntry(file.Hash, path))]) + .ConfigureAwait(false); + + CharaDataDownloadDto charaDataDownloadDto = new($"GPOSELOBBY:{CurrentGPoseLobbyId}", new(_apiController.UID)) + { + UpdatedDate = DateTime.UtcNow, + ManipulationData = playerData.ManipulationData, + CustomizeData = playerData.CustomizePlusData[API.Data.Enum.ObjectKind.Player], + FileGamePaths = filegamePaths, + FileSwaps = fileSwapPaths, + GlamourerData = playerData.GlamourerData[API.Data.Enum.ObjectKind.Player], + }; + + _lastCreatedCharaData = (playerData, charaDataDownloadDto); + } + + ForceResendOwnData(); + + if (_lastCreatedCharaData != null) + await _apiController.GposeLobbyPushCharacterData(_lastCreatedCharaData.Value.Dto) + .ConfigureAwait(false); + } + + internal void CreateNewLobby() + { + _ = Task.Run(async () => + { + ClearLobby(); + CurrentGPoseLobbyId = await _apiController.GposeLobbyCreate().ConfigureAwait(false); + if (!string.IsNullOrEmpty(CurrentGPoseLobbyId)) + { + _ = GposeWorldPositionBackgroundTask(_lobbyCts.Token); + _ = GposePoseDataBackgroundTask(_lobbyCts.Token); + } + }); + } + + internal void JoinGPoseLobby(string joinLobbyId, bool isReconnecting = false) + { + _ = Task.Run(async () => + { + var otherUsers = await _apiController.GposeLobbyJoin(joinLobbyId).ConfigureAwait(false); + ClearLobby(); + if (otherUsers.Any()) + { + LastGPoseLobbyId = string.Empty; + + foreach (var user in otherUsers) + { + OnUserJoinLobby(user); + } + + CurrentGPoseLobbyId = joinLobbyId; + _ = GposeWorldPositionBackgroundTask(_lobbyCts.Token); + _ = GposePoseDataBackgroundTask(_lobbyCts.Token); + } + else + { + LeaveGPoseLobby(); + LastGPoseLobbyId = string.Empty; + } + }); + } + + internal void LeaveGPoseLobby() + { + _ = Task.Run(async () => + { + var left = await _apiController.GposeLobbyLeave().ConfigureAwait(false); + if (left) + { + if (_usersInLobby.Count != 0) + { + LastGPoseLobbyId = CurrentGPoseLobbyId; + } + + ClearLobby(revertCharas: true); + } + }); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing) + { + ClearLobby(revertCharas: true); + } + } + + private void ClearLobby(bool revertCharas = false) + { + _lobbyCts.Cancel(); + _lobbyCts.Dispose(); + _lobbyCts = new(); + CurrentGPoseLobbyId = string.Empty; + foreach (var user in _usersInLobby.ToDictionary()) + { + if (revertCharas) + _charaDataManager.RevertChara(user.Value.HandledChara); + OnUserLeaveLobby(user.Value.UserData); + } + _usersInLobby.Clear(); + } + + private string CreateJsonFromPoseData(PoseData? poseData) + { + if (poseData == null) return "{}"; + + var node = new JsonObject(); + node["Bones"] = new JsonObject(); + foreach (var bone in poseData.Value.Bones) + { + node["Bones"]![bone.Key] = new JsonObject(); + node["Bones"]![bone.Key]!["Position"] = $"{bone.Value.PositionX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionZ.ToString(CultureInfo.InvariantCulture)}"; + node["Bones"]![bone.Key]!["Scale"] = $"{bone.Value.ScaleX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleZ.ToString(CultureInfo.InvariantCulture)}"; + node["Bones"]![bone.Key]!["Rotation"] = $"{bone.Value.RotationX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationZ.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationW.ToString(CultureInfo.InvariantCulture)}"; + } + node["MainHand"] = new JsonObject(); + foreach (var bone in poseData.Value.MainHand) + { + node["MainHand"]![bone.Key] = new JsonObject(); + node["MainHand"]![bone.Key]!["Position"] = $"{bone.Value.PositionX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionZ.ToString(CultureInfo.InvariantCulture)}"; + node["MainHand"]![bone.Key]!["Scale"] = $"{bone.Value.ScaleX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleZ.ToString(CultureInfo.InvariantCulture)}"; + node["MainHand"]![bone.Key]!["Rotation"] = $"{bone.Value.RotationX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationZ.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationW.ToString(CultureInfo.InvariantCulture)}"; + } + node["OffHand"] = new JsonObject(); + foreach (var bone in poseData.Value.OffHand) + { + node["OffHand"]![bone.Key] = new JsonObject(); + node["OffHand"]![bone.Key]!["Position"] = $"{bone.Value.PositionX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.PositionZ.ToString(CultureInfo.InvariantCulture)}"; + node["OffHand"]![bone.Key]!["Scale"] = $"{bone.Value.ScaleX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.ScaleZ.ToString(CultureInfo.InvariantCulture)}"; + node["OffHand"]![bone.Key]!["Rotation"] = $"{bone.Value.RotationX.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationY.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationZ.ToString(CultureInfo.InvariantCulture)}, {bone.Value.RotationW.ToString(CultureInfo.InvariantCulture)}"; + } + + return node.ToJsonString(); + } + + private PoseData CreatePoseDataFromJson(string json, PoseData? fullPoseData = null) + { + PoseData output = new(); + output.Bones = new(StringComparer.Ordinal); + output.MainHand = new(StringComparer.Ordinal); + output.OffHand = new(StringComparer.Ordinal); + + float getRounded(string number) + { + return float.Round(float.Parse(number, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture), 5); + } + + BoneData createBoneData(JsonNode boneJson) + { + BoneData outputBoneData = new(); + outputBoneData.Exists = true; + var posString = boneJson["Position"]!.ToString(); + var pos = posString.Split(",", StringSplitOptions.TrimEntries); + outputBoneData.PositionX = getRounded(pos[0]); + outputBoneData.PositionY = getRounded(pos[1]); + outputBoneData.PositionZ = getRounded(pos[2]); + + var scaString = boneJson["Scale"]!.ToString(); + var sca = scaString.Split(",", StringSplitOptions.TrimEntries); + outputBoneData.ScaleX = getRounded(sca[0]); + outputBoneData.ScaleY = getRounded(sca[1]); + outputBoneData.ScaleZ = getRounded(sca[2]); + + var rotString = boneJson["Rotation"]!.ToString(); + var rot = rotString.Split(",", StringSplitOptions.TrimEntries); + outputBoneData.RotationX = getRounded(rot[0]); + outputBoneData.RotationY = getRounded(rot[1]); + outputBoneData.RotationZ = getRounded(rot[2]); + outputBoneData.RotationW = getRounded(rot[3]); + return outputBoneData; + } + + var node = JsonNode.Parse(json)!; + var bones = node["Bones"]!.AsObject(); + foreach (var bone in bones) + { + string name = bone.Key; + var boneJson = bone.Value!.AsObject(); + BoneData outputBoneData = createBoneData(boneJson); + + if (fullPoseData != null) + { + if (fullPoseData.Value.Bones.TryGetValue(name, out var prevBoneData) && prevBoneData != outputBoneData) + { + output.Bones[name] = outputBoneData; + } + } + else + { + output.Bones[name] = outputBoneData; + } + } + var mainHand = node["MainHand"]!.AsObject(); + foreach (var bone in mainHand) + { + string name = bone.Key; + var boneJson = bone.Value!.AsObject(); + BoneData outputBoneData = createBoneData(boneJson); + + if (fullPoseData != null) + { + if (fullPoseData.Value.MainHand.TryGetValue(name, out var prevBoneData) && prevBoneData != outputBoneData) + { + output.MainHand[name] = outputBoneData; + } + } + else + { + output.MainHand[name] = outputBoneData; + } + } + var offhand = node["OffHand"]!.AsObject(); + foreach (var bone in offhand) + { + string name = bone.Key; + var boneJson = bone.Value!.AsObject(); + BoneData outputBoneData = createBoneData(boneJson); + + if (fullPoseData != null) + { + if (fullPoseData.Value.OffHand.TryGetValue(name, out var prevBoneData) && prevBoneData != outputBoneData) + { + output.OffHand[name] = outputBoneData; + } + } + else + { + output.OffHand[name] = outputBoneData; + } + } + + if (fullPoseData != null) + output.IsDelta = true; + + return output; + } + + private async Task GposePoseDataBackgroundTask(CancellationToken ct) + { + _lastFullPoseData = null; + _lastDeltaPoseData = null; + _poseGenerationExecutions = 0; + + while (!ct.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(2), ct).ConfigureAwait(false); + if (!_dalamudUtil.IsInGpose) continue; + if (_usersInLobby.Count == 0) continue; + + try + { + var chara = await _dalamudUtil.GetPlayerCharacterAsync().ConfigureAwait(false); + if (_dalamudUtil.IsInGpose) + { + chara = (IPlayerCharacter?)(await _dalamudUtil.GetGposeCharacterFromObjectTableByNameAsync(chara.Name.TextValue, _dalamudUtil.IsInGpose).ConfigureAwait(false)); + } + if (chara == null || chara.Address == nint.Zero) continue; + + var poseJson = await _brio.GetPoseAsync(chara.Address).ConfigureAwait(false); + if (string.IsNullOrEmpty(poseJson)) continue; + + var lastFullData = _poseGenerationExecutions++ >= 12 ? null : _lastFullPoseData; + lastFullData = _forceResendFullPose ? _lastFullPoseData : lastFullData; + + var poseData = CreatePoseDataFromJson(poseJson, lastFullData); + if (!poseData.IsDelta) + { + _lastFullPoseData = poseData; + _lastDeltaPoseData = null; + _poseGenerationExecutions = 0; + } + + bool deltaIsSame = _lastDeltaPoseData != null && + (poseData.Bones.Keys.All(k => _lastDeltaPoseData.Value.Bones.ContainsKey(k) + && poseData.Bones.Values.All(k => _lastDeltaPoseData.Value.Bones.ContainsValue(k)))); + + if (_forceResendFullPose || ((poseData.Bones.Any() || poseData.MainHand.Any() || poseData.OffHand.Any()) + && (!poseData.IsDelta || (poseData.IsDelta && !deltaIsSame)))) + { + _forceResendFullPose = false; + await _apiController.GposeLobbyPushPoseData(poseData).ConfigureAwait(false); + } + + if (poseData.IsDelta) + _lastDeltaPoseData = poseData; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error during Pose Data Generation"); + } + } + } + + private async Task GposeWorldPositionBackgroundTask(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(_dalamudUtil.IsInGpose ? 2 : 1), ct).ConfigureAwait(false); + + // if there are no players in lobby, don't do anything + if (_usersInLobby.Count == 0) continue; + + try + { + // get own player data + var player = (Dalamud.Game.ClientState.Objects.Types.ICharacter?)(await _dalamudUtil.GetPlayerCharacterAsync().ConfigureAwait(false)); + if (player == null) continue; + WorldData worldData; + if (_dalamudUtil.IsInGpose) + { + player = await _dalamudUtil.GetGposeCharacterFromObjectTableByNameAsync(player.Name.TextValue, true).ConfigureAwait(false); + if (player == null) continue; + worldData = (await _brio.GetTransformAsync(player.Address).ConfigureAwait(false)); + } + else + { + var rotQuaternion = Quaternion.CreateFromAxisAngle(new Vector3(0, 1, 0), player.Rotation); + worldData = new() + { + PositionX = player.Position.X, + PositionY = player.Position.Y, + PositionZ = player.Position.Z, + RotationW = rotQuaternion.W, + RotationX = rotQuaternion.X, + RotationY = rotQuaternion.Y, + RotationZ = rotQuaternion.Z, + ScaleX = 1, + ScaleY = 1, + ScaleZ = 1 + }; + } + + var loc = await _dalamudUtil.GetMapDataAsync().ConfigureAwait(false); + worldData.LocationInfo = loc; + + if (_forceResendWorldData || worldData != _lastWorldData) + { + _forceResendWorldData = false; + await _apiController.GposeLobbyPushWorldData(worldData).ConfigureAwait(false); + _lastWorldData = worldData; + Logger.LogTrace("WorldData (gpose: {gpose}): {data}", _dalamudUtil.IsInGpose, worldData); + } + + foreach (var entry in _usersInLobby) + { + if (!entry.Value.HasWorldDataUpdate || _dalamudUtil.IsInGpose || entry.Value.WorldData == null) continue; + + var entryWorldData = entry.Value.WorldData!.Value; + + if (worldData.LocationInfo.MapId == entryWorldData.LocationInfo.MapId && worldData.LocationInfo.DivisionId == entryWorldData.LocationInfo.DivisionId + && (worldData.LocationInfo.HouseId != entryWorldData.LocationInfo.HouseId + || worldData.LocationInfo.WardId != entryWorldData.LocationInfo.WardId + || entryWorldData.LocationInfo.ServerId != worldData.LocationInfo.ServerId)) + { + if (entry.Value.SpawnedVfxId == null) + { + // spawn if it doesn't exist yet + entry.Value.LastWorldPosition = new Vector3(entryWorldData.PositionX, entryWorldData.PositionY, entryWorldData.PositionZ); + entry.Value.SpawnedVfxId = await _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.SpawnObject(entry.Value.LastWorldPosition.Value, + Quaternion.Identity, Vector3.One, 0.5f, 0.1f, 0.5f, 0.9f)).ConfigureAwait(false); + } + else + { + // move object via lerp if it does exist + var newPosition = new Vector3(entryWorldData.PositionX, entryWorldData.PositionY, entryWorldData.PositionZ); + if (newPosition != entry.Value.LastWorldPosition) + { + entry.Value.UpdateStart = DateTime.UtcNow; + entry.Value.TargetWorldPosition = newPosition; + } + } + } + else + { + await _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.DespawnObject(entry.Value.SpawnedVfxId)).ConfigureAwait(false); + entry.Value.SpawnedVfxId = null; + } + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error during World Data Generation"); + } + } + } + + private void OnCutsceneFrameworkUpdate() + { + foreach (var kvp in _usersInLobby) + { + if (!string.IsNullOrWhiteSpace(kvp.Value.AssociatedCharaName)) + { + kvp.Value.Address = _dalamudUtil.GetGposeCharacterFromObjectTableByName(kvp.Value.AssociatedCharaName, true)?.Address ?? nint.Zero; + if (kvp.Value.Address == nint.Zero) + { + kvp.Value.AssociatedCharaName = string.Empty; + } + } + + if (kvp.Value.Address != nint.Zero && (kvp.Value.HasWorldDataUpdate || kvp.Value.HasPoseDataUpdate)) + { + bool hadPoseDataUpdate = kvp.Value.HasPoseDataUpdate; + bool hadWorldDataUpdate = kvp.Value.HasWorldDataUpdate; + kvp.Value.HasPoseDataUpdate = false; + kvp.Value.HasWorldDataUpdate = false; + + _ = Task.Run(async () => + { + if (hadPoseDataUpdate && kvp.Value.ApplicablePoseData != null) + { + await _brio.SetPoseAsync(kvp.Value.Address, CreateJsonFromPoseData(kvp.Value.ApplicablePoseData)).ConfigureAwait(false); + } + if (hadWorldDataUpdate && kvp.Value.WorldData != null) + { + await _brio.ApplyTransformAsync(kvp.Value.Address, kvp.Value.WorldData.Value).ConfigureAwait(false); + } + }); + } + } + } + + private void OnEnterGpose() + { + ForceResendOwnData(); + ResetOwnData(); + foreach (var data in _usersInLobby.Values) + { + _ = _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.DespawnObject(data.SpawnedVfxId)); + data.Reset(); + } + } + + private void OnExitGpose() + { + ForceResendOwnData(); + ResetOwnData(); + foreach (var data in _usersInLobby.Values) + { + data.Reset(); + } + } + + + private bool _forceResendFullPose = false; + private bool _forceResendWorldData = false; + + private void ForceResendOwnData() + { + _forceResendFullPose = true; + _forceResendWorldData = true; + } + + private void ResetOwnData() + { + _poseGenerationExecutions = 0; + _lastCreatedCharaData = null; + } + + private void OnFrameworkUpdate() + { + var frameworkTime = DateTime.UtcNow; + foreach (var kvp in _usersInLobby) + { + if (kvp.Value.SpawnedVfxId != null && kvp.Value.UpdateStart != null) + { + var secondsElasped = frameworkTime.Subtract(kvp.Value.UpdateStart.Value).TotalSeconds; + if (secondsElasped >= 1) + { + kvp.Value.LastWorldPosition = kvp.Value.TargetWorldPosition; + kvp.Value.TargetWorldPosition = null; + kvp.Value.UpdateStart = null; + } + else + { + var lerp = Vector3.Lerp(kvp.Value.LastWorldPosition ?? Vector3.One, kvp.Value.TargetWorldPosition ?? Vector3.One, (float)secondsElasped); + _vfxSpawnManager.MoveObject(kvp.Value.SpawnedVfxId.Value, lerp); + } + } + } + } + + private void OnReceiveCharaData(CharaDataDownloadDto charaDataDownloadDto) + { + if (!_usersInLobby.TryGetValue(charaDataDownloadDto.Uploader.UID, out var lobbyData)) + { + return; + } + + lobbyData.CharaData = charaDataDownloadDto; + if (lobbyData.Address != nint.Zero && !string.IsNullOrEmpty(lobbyData.AssociatedCharaName)) + { + _ = ApplyCharaData(lobbyData); + } + } + + public async Task ApplyCharaData(GposeLobbyUserData userData) + { + if (userData.CharaData == null || userData.Address == nint.Zero || string.IsNullOrEmpty(userData.AssociatedCharaName)) + return; + + await _charaDataCreationSemaphore.WaitAsync(_lobbyCts.Token).ConfigureAwait(false); + + try + { + await _charaDataManager.ApplyCharaData(userData.CharaData!, userData.AssociatedCharaName).ConfigureAwait(false); + userData.LastAppliedCharaDataDate = userData.CharaData.UpdatedDate; + userData.HasPoseDataUpdate = true; + userData.HasWorldDataUpdate = true; + } + finally + { + _charaDataCreationSemaphore.Release(); + } + } + + private readonly SemaphoreSlim _charaDataSpawnSemaphore = new(1, 1); + + internal async Task SpawnAndApplyData(GposeLobbyUserData userData) + { + if (userData.CharaData == null) + return; + + await _charaDataSpawnSemaphore.WaitAsync(_lobbyCts.Token).ConfigureAwait(false); + try + { + userData.HasPoseDataUpdate = false; + userData.HasWorldDataUpdate = false; + var chara = await _charaDataManager.SpawnAndApplyData(userData.CharaData).ConfigureAwait(false); + if (chara == null) return; + userData.HandledChara = chara; + userData.AssociatedCharaName = chara.Name; + userData.HasPoseDataUpdate = true; + userData.HasWorldDataUpdate = true; + } + finally + { + _charaDataSpawnSemaphore.Release(); + } + } + + private void OnReceivePoseData(UserData userData, PoseData poseData) + { + if (!_usersInLobby.TryGetValue(userData.UID, out var lobbyData)) + { + return; + } + + if (poseData.IsDelta) + lobbyData.DeltaPoseData = poseData; + else + lobbyData.FullPoseData = poseData; + } + + private void OnReceiveWorldData(UserData userData, WorldData worldData) + { + _usersInLobby[userData.UID].WorldData = worldData; + _ = _usersInLobby[userData.UID].SetWorldDataDescriptor(_dalamudUtil); + } + + private void OnUserJoinLobby(UserData userData) + { + if (_usersInLobby.ContainsKey(userData.UID)) + OnUserLeaveLobby(userData); + _usersInLobby[userData.UID] = new(userData); + _ = PushCharacterDownloadDto(); + } + + private void OnUserLeaveLobby(UserData msg) + { + _usersInLobby.Remove(msg.UID, out var existingData); + if (existingData != default) + { + _ = _dalamudUtil.RunOnFrameworkThread(() => _vfxSpawnManager.DespawnObject(existingData.SpawnedVfxId)); + } + } +} diff --git a/MareSynchronos/Services/CharaData/CharaDataManager.cs b/MareSynchronos/Services/CharaData/CharaDataManager.cs new file mode 100644 index 0000000..d87a9ca --- /dev/null +++ b/MareSynchronos/Services/CharaData/CharaDataManager.cs @@ -0,0 +1,1022 @@ +using Dalamud.Game.ClientState.Objects.Types; +using K4os.Compression.LZ4.Legacy; +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Factories; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.CharaData.Models; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Text; + +namespace MareSynchronos.Services; + +public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase +{ + private readonly ApiController _apiController; + private readonly CharaDataConfigService _configService; + private readonly DalamudUtilService _dalamudUtilService; + private readonly CharaDataFileHandler _fileHandler; + private readonly IpcManager _ipcManager; + private readonly ConcurrentDictionary _metaInfoCache = []; + private readonly List _nearbyData = []; + private readonly CharaDataNearbyManager _nearbyManager; + private readonly CharaDataCharacterHandler _characterHandler; + private readonly PairManager _pairManager; + private readonly Dictionary _ownCharaData = []; + private readonly Dictionary _sharedMetaInfoTimeoutTasks = []; + private readonly Dictionary> _sharedWithYouData = []; + private readonly Dictionary _updateDtos = []; + private CancellationTokenSource _applicationCts = new(); + private CancellationTokenSource _charaDataCreateCts = new(); + private CancellationTokenSource _connectCts = new(); + private CancellationTokenSource _getAllDataCts = new(); + private CancellationTokenSource _getSharedDataCts = new(); + private CancellationTokenSource _uploadCts = new(); + + public CharaDataManager(ILogger logger, ApiController apiController, + CharaDataFileHandler charaDataFileHandler, + MareMediator mareMediator, IpcManager ipcManager, DalamudUtilService dalamudUtilService, + FileDownloadManagerFactory fileDownloadManagerFactory, + CharaDataConfigService charaDataConfigService, CharaDataNearbyManager charaDataNearbyManager, + CharaDataCharacterHandler charaDataCharacterHandler, PairManager pairManager) : base(logger, mareMediator) + { + _apiController = apiController; + _fileHandler = charaDataFileHandler; + _ipcManager = ipcManager; + _dalamudUtilService = dalamudUtilService; + _configService = charaDataConfigService; + _nearbyManager = charaDataNearbyManager; + _characterHandler = charaDataCharacterHandler; + _pairManager = pairManager; + mareMediator.Subscribe(this, (msg) => + { + _connectCts?.Cancel(); + _connectCts?.Dispose(); + _connectCts = new(); + _ownCharaData.Clear(); + _metaInfoCache.Clear(); + _sharedWithYouData.Clear(); + _updateDtos.Clear(); + Initialized = false; + MaxCreatableCharaData = msg.Connection.ServerInfo.MaxCharaData; + if (_configService.Current.DownloadMcdDataOnConnection) + { + var token = _connectCts.Token; + _ = GetAllData(token); + _ = GetAllSharedData(token); + } + }); + mareMediator.Subscribe(this, (msg) => + { + _ownCharaData.Clear(); + _metaInfoCache.Clear(); + _sharedWithYouData.Clear(); + _updateDtos.Clear(); + Initialized = false; + }); + } + + public Task? AttachingPoseTask { get; private set; } + public Task? CharaUpdateTask { get; set; } + public string DataApplicationProgress { get; private set; } = string.Empty; + public Task? DataApplicationTask { get; private set; } + public Task<(string Output, bool Success)>? DataCreationTask { get; private set; } + public Task? DataGetTimeoutTask { get; private set; } + public Task<(string Result, bool Success)>? DownloadMetaInfoTask { get; private set; } + public Task>? GetAllDataTask { get; private set; } + public Task>? GetSharedWithYouTask { get; private set; } + public Task? GetSharedWithYouTimeoutTask { get; private set; } + public IReadOnlyDictionary HandledCharaData => _characterHandler.HandledCharaData; + public bool Initialized { get; private set; } + public CharaDataMetaInfoExtendedDto? LastDownloadedMetaInfo { get; private set; } + public Task<(MareCharaFileHeader LoadedFile, long ExpectedLength)>? LoadedMcdfHeader { get; private set; } + public int MaxCreatableCharaData { get; private set; } + public Task? McdfApplicationTask { get; private set; } + public List NearbyData => _nearbyData; + public IDictionary OwnCharaData => _ownCharaData; + public IDictionary> SharedWithYouData => _sharedWithYouData; + public Task? UiBlockingComputation { get; private set; } + public ValueProgress? UploadProgress { get; private set; } + public Task<(string Output, bool Success)>? UploadTask { get; set; } + public bool BrioAvailable => _ipcManager.Brio.APIAvailable; + + public Task ApplyCharaData(CharaDataDownloadDto dataDownloadDto, string charaName) + { + return UiBlockingComputation = DataApplicationTask = Task.Run(async () => + { + if (string.IsNullOrEmpty(charaName)) return; + + CharaDataMetaInfoDto metaInfo = new(dataDownloadDto.Id, dataDownloadDto.Uploader) + { + CanBeDownloaded = true, + Description = $"Data from {dataDownloadDto.Uploader.AliasOrUID} for {dataDownloadDto.Id}", + UpdatedDate = dataDownloadDto.UpdatedDate, + }; + + await DownloadAndAplyDataAsync(charaName, dataDownloadDto, metaInfo, false).ConfigureAwait(false); + }); + } + + public Task ApplyCharaData(CharaDataMetaInfoDto dataMetaInfoDto, string charaName) + { + return UiBlockingComputation = DataApplicationTask = Task.Run(async () => + { + if (string.IsNullOrEmpty(charaName)) return; + + var download = await _apiController.CharaDataDownload(dataMetaInfoDto.Uploader.UID + ":" + dataMetaInfoDto.Id).ConfigureAwait(false); + if (download == null) + { + DataApplicationTask = null; + return; + } + + await DownloadAndAplyDataAsync(charaName, download, dataMetaInfoDto, false).ConfigureAwait(false); + }); + } + + public Task ApplyCharaDataToGposeTarget(CharaDataMetaInfoDto dataMetaInfoDto) + { + return UiBlockingComputation = DataApplicationTask = Task.Run(async () => + { + var obj = await _dalamudUtilService.GetGposeTargetGameObjectAsync().ConfigureAwait(false); + var charaName = obj?.Name.TextValue ?? string.Empty; + if (string.IsNullOrEmpty(charaName)) return; + + await ApplyCharaData(dataMetaInfoDto, charaName).ConfigureAwait(false); + }); + } + + public async Task ApplyOwnDataToGposeTarget(CharaDataFullExtendedDto dataDto) + { + var chara = await _dalamudUtilService.GetGposeTargetGameObjectAsync().ConfigureAwait(false); + var charaName = chara?.Name.TextValue ?? string.Empty; + CharaDataDownloadDto downloadDto = new(dataDto.Id, dataDto.Uploader) + { + CustomizeData = dataDto.CustomizeData, + Description = dataDto.Description, + FileGamePaths = dataDto.FileGamePaths, + GlamourerData = dataDto.GlamourerData, + FileSwaps = dataDto.FileSwaps, + ManipulationData = dataDto.ManipulationData, + UpdatedDate = dataDto.UpdatedDate + }; + + CharaDataMetaInfoDto metaInfoDto = new(dataDto.Id, dataDto.Uploader) + { + CanBeDownloaded = true, + Description = dataDto.Description, + PoseData = dataDto.PoseData, + UpdatedDate = dataDto.UpdatedDate, + }; + + UiBlockingComputation = DataApplicationTask = DownloadAndAplyDataAsync(charaName, downloadDto, metaInfoDto, false); + } + + public Task ApplyPoseData(PoseEntry pose, string targetName) + { + return UiBlockingComputation = Task.Run(async () => + { + if (string.IsNullOrEmpty(pose.PoseData) || !(await CanApplyInGpose().ConfigureAwait(false)).CanApply) return; + var gposeChara = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(targetName, true).ConfigureAwait(false); + if (gposeChara == null) return; + + var poseJson = Encoding.UTF8.GetString(LZ4Wrapper.Unwrap(Convert.FromBase64String(pose.PoseData))); + if (string.IsNullOrEmpty(poseJson)) return; + + await _ipcManager.Brio.SetPoseAsync(gposeChara.Address, poseJson).ConfigureAwait(false); + }); + } + + public Task ApplyPoseDataToGPoseTarget(PoseEntry pose) + { + return UiBlockingComputation = Task.Run(async () => + { + var apply = await CanApplyInGpose().ConfigureAwait(false); + + if (apply.CanApply) + { + await ApplyPoseData(pose, apply.TargetName).ConfigureAwait(false); + } + }); + } + + public Task ApplyWorldDataToTarget(PoseEntry pose, string targetName) + { + return UiBlockingComputation = Task.Run(async () => + { + var apply = await CanApplyInGpose().ConfigureAwait(false); + if (pose.WorldData == default || !(await CanApplyInGpose().ConfigureAwait(false)).CanApply) return; + var gposeChara = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(targetName, true).ConfigureAwait(false); + if (gposeChara == null) return; + + if (pose.WorldData == null || pose.WorldData == default) return; + + Logger.LogDebug("Applying World data {data}", pose.WorldData); + + await _ipcManager.Brio.ApplyTransformAsync(gposeChara.Address, pose.WorldData.Value).ConfigureAwait(false); + }); + } + + public Task ApplyWorldDataToGPoseTarget(PoseEntry pose) + { + return UiBlockingComputation = Task.Run(async () => + { + var apply = await CanApplyInGpose().ConfigureAwait(false); + if (apply.CanApply) + { + await ApplyPoseData(pose, apply.TargetName).ConfigureAwait(false); + } + }); + } + + public void AttachWorldData(PoseEntry pose, CharaDataExtendedUpdateDto updateDto) + { + AttachingPoseTask = Task.Run(async () => + { + ICharacter? playerChar = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false); + if (playerChar == null) return; + if (_dalamudUtilService.IsInGpose) + { + playerChar = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(playerChar.Name.TextValue, true).ConfigureAwait(false); + } + if (playerChar == null) return; + var worldData = await _ipcManager.Brio.GetTransformAsync(playerChar.Address).ConfigureAwait(false); + if (worldData == default) return; + + Logger.LogTrace("Attaching World data {data}", worldData); + + worldData.LocationInfo = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false); + + Logger.LogTrace("World data serialized: {data}", worldData); + + pose.WorldData = worldData; + + updateDto.UpdatePoseList(); + }); + } + + public async Task<(bool CanApply, string TargetName)> CanApplyInGpose() + { + var obj = await _dalamudUtilService.GetGposeTargetGameObjectAsync().ConfigureAwait(false); + string targetName = string.Empty; + bool canApply = _dalamudUtilService.IsInGpose && obj != null + && obj.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player; + if (canApply) + { + targetName = obj!.Name.TextValue; + } + else + { + targetName = "Invalid Target"; + } + return (canApply, targetName); + } + + public void CancelDataApplication() + { + _applicationCts.Cancel(); + } + + public void CancelUpload() + { + _uploadCts.Cancel(); + } + + public void CreateCharaDataEntry(CancellationToken cancelToken) + { + UiBlockingComputation = DataCreationTask = Task.Run(async () => + { + var result = await _apiController.CharaDataCreate().ConfigureAwait(false); + _ = Task.Run(async () => + { + _charaDataCreateCts = _charaDataCreateCts.CancelRecreate(); + using var ct = CancellationTokenSource.CreateLinkedTokenSource(_charaDataCreateCts.Token, cancelToken); + await Task.Delay(TimeSpan.FromSeconds(10), ct.Token).ConfigureAwait(false); + DataCreationTask = null; + }); + + + if (result == null) + return ("Failed to create character data, see log for more information", false); + + await AddOrUpdateDto(result).ConfigureAwait(false); + + return ("Created Character Data", true); + }); + } + + public async Task DeleteCharaData(CharaDataFullExtendedDto dto) + { + var ret = await _apiController.CharaDataDelete(dto.Id).ConfigureAwait(false); + if (ret) + { + _ownCharaData.Remove(dto.Id); + _metaInfoCache.Remove(dto.FullId, out _); + } + DistributeMetaInfo(); + } + + public void DownloadMetaInfo(string importCode, bool store = true) + { + DownloadMetaInfoTask = Task.Run(async () => + { + try + { + if (store) + { + LastDownloadedMetaInfo = null; + } + var metaInfo = await _apiController.CharaDataGetMetainfo(importCode).ConfigureAwait(false); + _sharedMetaInfoTimeoutTasks[importCode] = Task.Delay(TimeSpan.FromSeconds(10)); + if (metaInfo == null) + { + _metaInfoCache[importCode] = null; + return ("Failed to download meta info for this code. Check if the code is valid and you have rights to access it.", false); + } + await CacheData(metaInfo).ConfigureAwait(false); + if (store) + { + LastDownloadedMetaInfo = await CharaDataMetaInfoExtendedDto.Create(metaInfo, _dalamudUtilService).ConfigureAwait(false); + } + return ("Ok", true); + } + finally + { + if (!store) + DownloadMetaInfoTask = null; + } + }); + } + + public async Task GetAllData(CancellationToken cancelToken) + { + foreach (var data in _ownCharaData) + { + _metaInfoCache.Remove(data.Key, out _); + } + _ownCharaData.Clear(); + UiBlockingComputation = GetAllDataTask = Task.Run(async () => + { + _getAllDataCts = _getAllDataCts.CancelRecreate(); + var result = await _apiController.CharaDataGetOwn().ConfigureAwait(false); + + Initialized = true; + + if (result.Any()) + { + DataGetTimeoutTask = Task.Run(async () => + { + using var ct = CancellationTokenSource.CreateLinkedTokenSource(_getAllDataCts.Token, cancelToken); +#if !DEBUG + await Task.Delay(TimeSpan.FromMinutes(1), ct.Token).ConfigureAwait(false); +#else + await Task.Delay(TimeSpan.FromSeconds(5), ct.Token).ConfigureAwait(false); +#endif + }); + } + + return result.OrderBy(u => u.CreatedDate).Select(k => new CharaDataFullExtendedDto(k)).ToList(); + }); + + var result = await GetAllDataTask.ConfigureAwait(false); + foreach (var item in result) + { + await AddOrUpdateDto(item).ConfigureAwait(false); + } + + foreach (var id in _updateDtos.Keys.Where(r => !result.Exists(res => string.Equals(res.Id, r, StringComparison.Ordinal))).ToList()) + { + _updateDtos.Remove(id); + } + GetAllDataTask = null; + } + + public async Task GetAllSharedData(CancellationToken token) + { + Logger.LogDebug("Getting Shared with You Data"); + + UiBlockingComputation = GetSharedWithYouTask = _apiController.CharaDataGetShared(); + _sharedWithYouData.Clear(); + + GetSharedWithYouTimeoutTask = Task.Run(async () => + { + _getSharedDataCts = _getSharedDataCts.CancelRecreate(); + using var ct = CancellationTokenSource.CreateLinkedTokenSource(_getSharedDataCts.Token, token); +#if !DEBUG + await Task.Delay(TimeSpan.FromMinutes(1), ct.Token).ConfigureAwait(false); +#else + await Task.Delay(TimeSpan.FromSeconds(5), ct.Token).ConfigureAwait(false); +#endif + GetSharedWithYouTimeoutTask = null; + Logger.LogDebug("Finished Shared with You Data Timeout"); + }); + + var result = await GetSharedWithYouTask.ConfigureAwait(false); + foreach (var grouping in result.GroupBy(r => r.Uploader)) + { + var pair = _pairManager.GetPairByUID(grouping.Key.UID); + if (pair?.IsPaused ?? false) continue; + List newList = new(); + foreach (var item in grouping) + { + var extended = await CharaDataMetaInfoExtendedDto.Create(item, _dalamudUtilService).ConfigureAwait(false); + newList.Add(extended); + CacheData(extended); + } + _sharedWithYouData[grouping.Key] = newList; + } + + DistributeMetaInfo(); + + Logger.LogDebug("Finished getting Shared with You Data"); + GetSharedWithYouTask = null; + } + + public CharaDataExtendedUpdateDto? GetUpdateDto(string id) + { + if (_updateDtos.TryGetValue(id, out var dto)) + return dto; + return null; + } + + public bool IsInTimeout(string key) + { + if (!_sharedMetaInfoTimeoutTasks.TryGetValue(key, out var task)) return false; + return !task?.IsCompleted ?? false; + } + + public void LoadMcdf(string filePath) + { + LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(filePath); + } + + public void McdfApplyToTarget(string charaName) + { + if (LoadedMcdfHeader == null || !LoadedMcdfHeader.IsCompletedSuccessfully) return; + + List actuallyExtractedFiles = []; + + UiBlockingComputation = McdfApplicationTask = Task.Run(async () => + { + Guid applicationId = Guid.NewGuid(); + try + { + using GameObjectHandler? tempHandler = await _characterHandler.TryCreateGameObjectHandler(charaName, true).ConfigureAwait(false); + if (tempHandler == null) return; + var playerChar = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false); + bool isSelf = playerChar != null && string.Equals(playerChar.Name.TextValue, tempHandler.Name, StringComparison.Ordinal); + + long expectedExtractedSize = LoadedMcdfHeader.Result.ExpectedLength; + var charaFile = LoadedMcdfHeader.Result.LoadedFile; + DataApplicationProgress = "Extracting MCDF data"; + + var extractedFiles = _fileHandler.McdfExtractFiles(charaFile, expectedExtractedSize, actuallyExtractedFiles); + + foreach (var entry in charaFile.CharaFileData.FileSwaps.SelectMany(k => k.GamePaths, (k, p) => new KeyValuePair(p, k.FileSwapPath))) + { + extractedFiles[entry.Key] = entry.Value; + } + + DataApplicationProgress = "Applying MCDF data"; + + var extended = await CharaDataMetaInfoExtendedDto.Create(new(charaFile.FilePath, new UserData(string.Empty)), _dalamudUtilService) + .ConfigureAwait(false); + await ApplyDataAsync(applicationId, tempHandler, isSelf, autoRevert: false, extended, + extractedFiles, charaFile.CharaFileData.ManipulationData, charaFile.CharaFileData.GlamourerData, + charaFile.CharaFileData.CustomizePlusData, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to extract MCDF"); + throw; + } + finally + { + // delete extracted files + foreach (var file in actuallyExtractedFiles) + { + File.Delete(file); + } + } + }); + } + + public async Task McdfApplyToGposeTarget() + { + var apply = await CanApplyInGpose().ConfigureAwait(false); + if (apply.CanApply) + { + McdfApplyToTarget(apply.TargetName); + } + } + + public void SaveMareCharaFile(string description, string filePath) + { + UiBlockingComputation = Task.Run(async () => await _fileHandler.SaveCharaFileAsync(description, filePath).ConfigureAwait(false)); + } + + public void SetAppearanceData(string dtoId) + { + var hasDto = _ownCharaData.TryGetValue(dtoId, out var dto); + if (!hasDto || dto == null) return; + + var hasUpdateDto = _updateDtos.TryGetValue(dtoId, out var updateDto); + if (!hasUpdateDto || updateDto == null) return; + + UiBlockingComputation = Task.Run(async () => + { + await _fileHandler.UpdateCharaDataAsync(updateDto).ConfigureAwait(false); + }); + } + + public Task SpawnAndApplyData(CharaDataDownloadDto charaDataDownloadDto) + { + var task = Task.Run(async () => + { + var newActor = await _ipcManager.Brio.SpawnActorAsync().ConfigureAwait(false); + if (newActor == null) return null; + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + + await ApplyCharaData(charaDataDownloadDto, newActor.Name.TextValue).ConfigureAwait(false); + + return _characterHandler.HandledCharaData.GetValueOrDefault(newActor.Name.TextValue); + }); + UiBlockingComputation = task; + return task; + } + + public Task SpawnAndApplyData(CharaDataMetaInfoDto charaDataMetaInfoDto) + { + var task = Task.Run(async () => + { + var newActor = await _ipcManager.Brio.SpawnActorAsync().ConfigureAwait(false); + if (newActor == null) return null; + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + + await ApplyCharaData(charaDataMetaInfoDto, newActor.Name.TextValue).ConfigureAwait(false); + + return _characterHandler.HandledCharaData.GetValueOrDefault(newActor.Name.TextValue); + }); + UiBlockingComputation = task; + return task; + } + + private async Task CacheData(CharaDataFullExtendedDto ownCharaData) + { + var metaInfo = new CharaDataMetaInfoDto(ownCharaData.Id, ownCharaData.Uploader) + { + Description = ownCharaData.Description, + UpdatedDate = ownCharaData.UpdatedDate, + CanBeDownloaded = !string.IsNullOrEmpty(ownCharaData.GlamourerData) && (ownCharaData.OriginalFiles.Count == ownCharaData.FileGamePaths.Count), + PoseData = ownCharaData.PoseData, + }; + + var extended = await CharaDataMetaInfoExtendedDto.Create(metaInfo, _dalamudUtilService, isOwnData: true).ConfigureAwait(false); + _metaInfoCache[extended.FullId] = extended; + DistributeMetaInfo(); + + return extended; + } + + private async Task CacheData(CharaDataMetaInfoDto metaInfo, bool isOwnData = false) + { + var extended = await CharaDataMetaInfoExtendedDto.Create(metaInfo, _dalamudUtilService, isOwnData).ConfigureAwait(false); + _metaInfoCache[extended.FullId] = extended; + DistributeMetaInfo(); + + return extended; + } + + private readonly SemaphoreSlim _distributionSemaphore = new(1, 1); + + private void DistributeMetaInfo() + { + _distributionSemaphore.Wait(); + _nearbyManager.UpdateSharedData(_metaInfoCache.ToDictionary()); + _characterHandler.UpdateHandledData(_metaInfoCache.ToDictionary()); + _distributionSemaphore.Release(); + } + + private void CacheData(CharaDataMetaInfoExtendedDto charaData) + { + _metaInfoCache[charaData.FullId] = charaData; + } + + public bool TryGetMetaInfo(string key, out CharaDataMetaInfoExtendedDto? metaInfo) + { + return _metaInfoCache.TryGetValue(key, out metaInfo); + } + + public void UploadCharaData(string id) + { + var hasUpdateDto = _updateDtos.TryGetValue(id, out var updateDto); + if (!hasUpdateDto || updateDto == null) return; + + UiBlockingComputation = CharaUpdateTask = CharaUpdateAsync(updateDto); + } + + public void UploadMissingFiles(string id) + { + var hasDto = _ownCharaData.TryGetValue(id, out var dto); + if (!hasDto || dto == null) return; + + UiBlockingComputation = UploadTask = RestoreThenUpload(dto); + } + + private async Task<(string Output, bool Success)> RestoreThenUpload(CharaDataFullExtendedDto dto) + { + var newDto = await _apiController.CharaDataAttemptRestore(dto.Id).ConfigureAwait(false); + if (newDto == null) + { + _ownCharaData.Remove(dto.Id); + _metaInfoCache.Remove(dto.FullId, out _); + UiBlockingComputation = null; + return ("No such DTO found", false); + } + + await AddOrUpdateDto(newDto).ConfigureAwait(false); + _ = _ownCharaData.TryGetValue(dto.Id, out var extendedDto); + + if (!extendedDto!.HasMissingFiles) + { + UiBlockingComputation = null; + return ("Restored successfully", true); + } + + var missingFileList = extendedDto!.MissingFiles.ToList(); + var result = await UploadFiles(missingFileList, async () => + { + var newFilePaths = dto.FileGamePaths; + foreach (var missing in missingFileList) + { + newFilePaths.Add(missing); + } + CharaDataUpdateDto updateDto = new(dto.Id) + { + FileGamePaths = newFilePaths + }; + var res = await _apiController.CharaDataUpdate(updateDto).ConfigureAwait(false); + await AddOrUpdateDto(res).ConfigureAwait(false); + }).ConfigureAwait(false); + + UiBlockingComputation = null; + return result; + } + + internal void ApplyDataToSelf(CharaDataFullExtendedDto dataDto) + { + var chara = _dalamudUtilService.GetPlayerName(); + CharaDataDownloadDto downloadDto = new(dataDto.Id, dataDto.Uploader) + { + CustomizeData = dataDto.CustomizeData, + Description = dataDto.Description, + FileGamePaths = dataDto.FileGamePaths, + GlamourerData = dataDto.GlamourerData, + FileSwaps = dataDto.FileSwaps, + ManipulationData = dataDto.ManipulationData, + UpdatedDate = dataDto.UpdatedDate + }; + + CharaDataMetaInfoDto metaInfoDto = new(dataDto.Id, dataDto.Uploader) + { + CanBeDownloaded = true, + Description = dataDto.Description, + PoseData = dataDto.PoseData, + UpdatedDate = dataDto.UpdatedDate, + }; + + UiBlockingComputation = DataApplicationTask = DownloadAndAplyDataAsync(chara, downloadDto, metaInfoDto); + } + + internal void AttachPoseData(PoseEntry pose, CharaDataExtendedUpdateDto updateDto) + { + AttachingPoseTask = Task.Run(async () => + { + ICharacter? playerChar = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false); + if (playerChar == null) return; + if (_dalamudUtilService.IsInGpose) + { + playerChar = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(playerChar.Name.TextValue, true).ConfigureAwait(false); + } + if (playerChar == null) return; + var poseData = await _ipcManager.Brio.GetPoseAsync(playerChar.Address).ConfigureAwait(false); + if (poseData == null) return; + + var compressedByteData = LZ4Wrapper.WrapHC(Encoding.UTF8.GetBytes(poseData)); + pose.PoseData = Convert.ToBase64String(compressedByteData); + updateDto.UpdatePoseList(); + }); + } + + internal void McdfSpawnApplyToGposeTarget() + { + UiBlockingComputation = Task.Run(async () => + { + var newActor = await _ipcManager.Brio.SpawnActorAsync().ConfigureAwait(false); + if (newActor == null) return; + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + unsafe + { + _dalamudUtilService.GposeTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)newActor.Address; + } + + await McdfApplyToGposeTarget().ConfigureAwait(false); + }); + } + + internal void ApplyFullPoseDataToTarget(PoseEntry value, string targetName) + { + UiBlockingComputation = Task.Run(async () => + { + await ApplyPoseData(value, targetName).ConfigureAwait(false); + await ApplyWorldDataToTarget(value, targetName).ConfigureAwait(false); + }); + } + + internal void ApplyFullPoseDataToGposeTarget(PoseEntry value) + { + UiBlockingComputation = Task.Run(async () => + { + var apply = await CanApplyInGpose().ConfigureAwait(false); + if (apply.CanApply) + { + await ApplyPoseData(value, apply.TargetName).ConfigureAwait(false); + await ApplyWorldDataToTarget(value, apply.TargetName).ConfigureAwait(false); + } + }); + } + + internal void SpawnAndApplyWorldTransform(CharaDataMetaInfoDto metaInfo, PoseEntry value) + { + UiBlockingComputation = Task.Run(async () => + { + var actor = await SpawnAndApplyData(metaInfo).ConfigureAwait(false); + if (actor == null) return; + await ApplyPoseData(value, actor.Name).ConfigureAwait(false); + await ApplyWorldDataToTarget(value, actor.Name).ConfigureAwait(false); + }); + } + + internal unsafe void TargetGposeActor(HandledCharaDataEntry actor) + { + var gposeActor = _dalamudUtilService.GetGposeCharacterFromObjectTableByName(actor.Name, true); + if (gposeActor != null) + { + _dalamudUtilService.GposeTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gposeActor.Address; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing) + { + _getAllDataCts?.Cancel(); + _getAllDataCts?.Dispose(); + _getSharedDataCts?.Cancel(); + _getSharedDataCts?.Dispose(); + _charaDataCreateCts?.Cancel(); + _charaDataCreateCts?.Dispose(); + _uploadCts?.Cancel(); + _uploadCts?.Dispose(); + _applicationCts.Cancel(); + _applicationCts.Dispose(); + _connectCts?.Cancel(); + _connectCts?.Dispose(); + } + } + + private async Task AddOrUpdateDto(CharaDataFullDto? dto) + { + if (dto == null) return; + + _ownCharaData[dto.Id] = new(dto); + _updateDtos[dto.Id] = new(new(dto.Id), _ownCharaData[dto.Id]); + + await CacheData(_ownCharaData[dto.Id]).ConfigureAwait(false); + } + + private async Task ApplyDataAsync(Guid applicationId, GameObjectHandler tempHandler, bool isSelf, bool autoRevert, + CharaDataMetaInfoExtendedDto metaInfo, Dictionary modPaths, string? manipData, string? glamourerData, string? customizeData, CancellationToken token) + { + Guid? cPlusId = null; + Guid penumbraCollection; + try + { + DataApplicationProgress = "Reverting previous Application"; + + Logger.LogTrace("[{appId}] Reverting chara {chara}", applicationId, tempHandler.Name); + bool reverted = await _characterHandler.RevertHandledChara(tempHandler.Name).ConfigureAwait(false); + if (reverted) + await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false); + + Logger.LogTrace("[{appId}] Applying data in Penumbra", applicationId); + + DataApplicationProgress = "Applying Penumbra information"; + penumbraCollection = await _ipcManager.Penumbra.CreateTemporaryCollectionAsync(Logger, metaInfo.Uploader.UID + metaInfo.Id).ConfigureAwait(false); + var idx = await _dalamudUtilService.RunOnFrameworkThread(() => tempHandler.GetGameObject()?.ObjectIndex).ConfigureAwait(false) ?? 0; + await _ipcManager.Penumbra.AssignTemporaryCollectionAsync(Logger, penumbraCollection, idx).ConfigureAwait(false); + await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, applicationId, penumbraCollection, modPaths).ConfigureAwait(false); + await _ipcManager.Penumbra.SetManipulationDataAsync(Logger, applicationId, penumbraCollection, manipData ?? string.Empty).ConfigureAwait(false); + + Logger.LogTrace("[{appId}] Applying Glamourer data and Redrawing", applicationId); + DataApplicationProgress = "Applying Glamourer and redrawing Character"; + await _ipcManager.Glamourer.ApplyAllAsync(Logger, tempHandler, glamourerData, applicationId, token).ConfigureAwait(false); + await _ipcManager.Penumbra.RedrawAsync(Logger, tempHandler, applicationId, token).ConfigureAwait(false); + await _dalamudUtilService.WaitWhileCharacterIsDrawing(Logger, tempHandler, applicationId, ct: token).ConfigureAwait(false); + Logger.LogTrace("[{appId}] Removing collection", applicationId); + await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, penumbraCollection).ConfigureAwait(false); + + DataApplicationProgress = "Applying Customize+ data"; + Logger.LogTrace("[{appId}] Appplying C+ data", applicationId); + + if (!string.IsNullOrEmpty(customizeData)) + { + cPlusId = await _ipcManager.CustomizePlus.SetBodyScaleAsync(tempHandler.Address, customizeData).ConfigureAwait(false); + } + else + { + cPlusId = await _ipcManager.CustomizePlus.SetBodyScaleAsync(tempHandler.Address, Convert.ToBase64String(Encoding.UTF8.GetBytes("{}"))).ConfigureAwait(false); + } + + if (autoRevert) + { + Logger.LogTrace("[{appId}] Starting wait for auto revert", applicationId); + + int i = 15; + while (i > 0) + { + DataApplicationProgress = $"All data applied. Reverting automatically in {i} seconds."; + await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); + i--; + } + } + else + { + Logger.LogTrace("[{appId}] Adding {name} to handled objects", applicationId, tempHandler.Name); + + _characterHandler.AddHandledChara(new HandledCharaDataEntry(tempHandler.Name, isSelf, cPlusId, metaInfo)); + } + } + finally + { + if (token.IsCancellationRequested) + DataApplicationProgress = "Application aborted. Reverting Character..."; + else if (autoRevert) + DataApplicationProgress = "Application finished. Reverting Character..."; + if (autoRevert) + { + await _characterHandler.RevertChara(tempHandler.Name, cPlusId).ConfigureAwait(false); + } + + if (!_dalamudUtilService.IsInGpose) + Mediator.Publish(new HaltCharaDataCreation(Resume: true)); + + if (metaInfo != null && _configService.Current.FavoriteCodes.TryGetValue(metaInfo.Uploader.UID + ":" + metaInfo.Id, out var favorite) && favorite != null) + { + favorite.LastDownloaded = DateTime.UtcNow; + _configService.Save(); + } + + DataApplicationTask = null; + DataApplicationProgress = string.Empty; + } + } + + private async Task CharaUpdateAsync(CharaDataExtendedUpdateDto updateDto) + { + Logger.LogDebug("Uploading Chara Data to Server"); + var baseUpdateDto = updateDto.BaseDto; + if (baseUpdateDto.FileGamePaths != null) + { + Logger.LogDebug("Detected file path changes, starting file upload"); + + UploadTask = UploadFiles(baseUpdateDto.FileGamePaths); + var result = await UploadTask.ConfigureAwait(false); + if (!result.Success) + { + return; + } + } + + Logger.LogDebug("Pushing update dto to server: {data}", baseUpdateDto); + + var res = await _apiController.CharaDataUpdate(baseUpdateDto).ConfigureAwait(false); + await AddOrUpdateDto(res).ConfigureAwait(false); + CharaUpdateTask = null; + } + + private async Task DownloadAndAplyDataAsync(string charaName, CharaDataDownloadDto charaDataDownloadDto, CharaDataMetaInfoDto metaInfo, bool autoRevert = true) + { + _applicationCts = _applicationCts.CancelRecreate(); + var token = _applicationCts.Token; + ICharacter? chara = (await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(charaName, _dalamudUtilService.IsInGpose).ConfigureAwait(false)); + + if (chara == null) + return; + + var applicationId = Guid.NewGuid(); + + var playerChar = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false); + bool isSelf = playerChar != null && string.Equals(playerChar.Name.TextValue, chara.Name.TextValue, StringComparison.Ordinal); + + DataApplicationProgress = "Checking local files"; + + Logger.LogTrace("[{appId}] Computing local missing files", applicationId); + + Dictionary modPaths; + List missingFiles; + _fileHandler.ComputeMissingFiles(charaDataDownloadDto, out modPaths, out missingFiles); + + Logger.LogTrace("[{appId}] Computing local missing files", applicationId); + + using GameObjectHandler? tempHandler = await _characterHandler.TryCreateGameObjectHandler(chara.ObjectIndex).ConfigureAwait(false); + if (tempHandler == null) return; + + if (missingFiles.Any()) + { + try + { + DataApplicationProgress = "Downloading Missing Files. Please be patient."; + await _fileHandler.DownloadFilesAsync(tempHandler, missingFiles, modPaths, token).ConfigureAwait(false); + } + catch (FileNotFoundException) + { + DataApplicationProgress = "Failed to download one or more files. Aborting."; + DataApplicationTask = null; + return; + } + catch (OperationCanceledException) + { + DataApplicationProgress = "Application aborted."; + DataApplicationTask = null; + return; + } + } + + if (!_dalamudUtilService.IsInGpose) + Mediator.Publish(new HaltCharaDataCreation()); + + var extendedMetaInfo = await CacheData(metaInfo).ConfigureAwait(false); + + await ApplyDataAsync(applicationId, tempHandler, isSelf, autoRevert, extendedMetaInfo, modPaths, charaDataDownloadDto.ManipulationData, charaDataDownloadDto.GlamourerData, + charaDataDownloadDto.CustomizeData, token).ConfigureAwait(false); + } + + public async Task<(string Result, bool Success)> UploadFiles(List missingFileList, Func? postUpload = null) + { + UploadProgress = new ValueProgress(); + try + { + _uploadCts = _uploadCts.CancelRecreate(); + var missingFiles = await _fileHandler.UploadFiles([.. missingFileList.Select(k => k.HashOrFileSwap)], UploadProgress, _uploadCts.Token).ConfigureAwait(false); + if (missingFiles.Any()) + { + Logger.LogInformation("Failed to upload {files}", string.Join(", ", missingFiles)); + return ($"Upload failed: {missingFiles.Count} missing or forbidden to upload local files.", false); + } + + if (postUpload != null) + await postUpload.Invoke().ConfigureAwait(false); + + return ("Upload sucessful", true); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error during upload"); + if (ex is OperationCanceledException) + { + return ("Upload Cancelled", false); + } + return ("Error in upload, see log for more details", false); + } + finally + { + UiBlockingComputation = null; + } + } + + public void RevertChara(HandledCharaDataEntry? handled) + { + UiBlockingComputation = _characterHandler.RevertHandledChara(handled); + } + + internal void RemoveChara(string handledActor) + { + if (string.IsNullOrEmpty(handledActor)) return; + UiBlockingComputation = Task.Run(async () => + { + await _characterHandler.RevertHandledChara(handledActor).ConfigureAwait(false); + var gposeChara = await _dalamudUtilService.GetGposeCharacterFromObjectTableByNameAsync(handledActor, true).ConfigureAwait(false); + if (gposeChara != null) + await _ipcManager.Brio.DespawnActorAsync(gposeChara.Address).ConfigureAwait(false); + }); + } +} diff --git a/MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs b/MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs new file mode 100644 index 0000000..8259446 --- /dev/null +++ b/MareSynchronos/Services/CharaData/CharaDataNearbyManager.cs @@ -0,0 +1,296 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using MareSynchronos.API.Data; +using MareSynchronos.Interop; +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services.CharaData.Models; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using Microsoft.Extensions.Logging; +using System.Numerics; + +namespace MareSynchronos.Services; + +public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase +{ + public record NearbyCharaDataEntry + { + public float Direction { get; init; } + public float Distance { get; init; } + } + + private readonly DalamudUtilService _dalamudUtilService; + private readonly Dictionary _nearbyData = []; + private readonly Dictionary _poseVfx = []; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly CharaDataConfigService _charaDataConfigService; + private readonly Dictionary> _metaInfoCache = []; + private readonly VfxSpawnManager _vfxSpawnManager; + private Task? _filterEntriesRunningTask; + private (Guid VfxId, PoseEntryExtended Pose)? _hoveredVfx = null; + private DateTime _lastExecutionTime = DateTime.UtcNow; + private SemaphoreSlim _sharedDataUpdateSemaphore = new(1, 1); + public CharaDataNearbyManager(ILogger logger, MareMediator mediator, + DalamudUtilService dalamudUtilService, VfxSpawnManager vfxSpawnManager, + ServerConfigurationManager serverConfigurationManager, + CharaDataConfigService charaDataConfigService) : base(logger, mediator) + { + mediator.Subscribe(this, (_) => HandleFrameworkUpdate()); + mediator.Subscribe(this, (_) => HandleFrameworkUpdate()); + _dalamudUtilService = dalamudUtilService; + _vfxSpawnManager = vfxSpawnManager; + _serverConfigurationManager = serverConfigurationManager; + _charaDataConfigService = charaDataConfigService; + mediator.Subscribe(this, (_) => ClearAllVfx()); + } + + public bool ComputeNearbyData { get; set; } = false; + + public IDictionary NearbyData => _nearbyData; + + public string UserNoteFilter { get; set; } = string.Empty; + + public void UpdateSharedData(Dictionary newData) + { + _sharedDataUpdateSemaphore.Wait(); + try + { + _metaInfoCache.Clear(); + foreach (var kvp in newData) + { + if (kvp.Value == null) continue; + + if (!_metaInfoCache.TryGetValue(kvp.Value.Uploader, out var list)) + { + _metaInfoCache[kvp.Value.Uploader] = list = []; + } + + list.Add(kvp.Value); + } + } + finally + { + _sharedDataUpdateSemaphore.Release(); + } + } + + internal void SetHoveredVfx(PoseEntryExtended? hoveredPose) + { + if (hoveredPose == null && _hoveredVfx == null) + return; + + if (hoveredPose == null) + { + _vfxSpawnManager.DespawnObject(_hoveredVfx!.Value.VfxId); + _hoveredVfx = null; + return; + } + + if (_hoveredVfx == null) + { + var vfxGuid = _vfxSpawnManager.SpawnObject(hoveredPose.Position, hoveredPose.Rotation, Vector3.One * 4, 1, 0.2f, 0.2f, 1f); + if (vfxGuid != null) + _hoveredVfx = (vfxGuid.Value, hoveredPose); + return; + } + + if (hoveredPose != _hoveredVfx!.Value.Pose) + { + _vfxSpawnManager.DespawnObject(_hoveredVfx.Value.VfxId); + var vfxGuid = _vfxSpawnManager.SpawnObject(hoveredPose.Position, hoveredPose.Rotation, Vector3.One * 4, 1, 0.2f, 0.2f, 1f); + if (vfxGuid != null) + _hoveredVfx = (vfxGuid.Value, hoveredPose); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + ClearAllVfx(); + } + + private static float CalculateYawDegrees(Vector3 directionXZ) + { + // Calculate yaw angle in radians using Atan2 (X, Z) + float yawRadians = (float)Math.Atan2(-directionXZ.X, directionXZ.Z); + float yawDegrees = yawRadians * (180f / (float)Math.PI); + + // Normalize to [0, 360) + if (yawDegrees < 0) + yawDegrees += 360f; + + return yawDegrees; + } + + private static float GetAngleToTarget(Vector3 cameraPosition, float cameraYawDegrees, Vector3 targetPosition) + { + // Step 4: Calculate the direction vector from camera to target + Vector3 directionToTarget = targetPosition - cameraPosition; + + // Step 5: Project the directionToTarget onto the XZ plane (ignore Y) + Vector3 directionToTargetXZ = new Vector3(directionToTarget.X, 0, directionToTarget.Z); + + // Handle the case where the target is directly above or below the camera + if (directionToTargetXZ.LengthSquared() < 1e-10f) + { + return 0; // Default direction + } + + directionToTargetXZ = Vector3.Normalize(directionToTargetXZ); + + // Step 6: Calculate the target's yaw angle + float targetYawDegrees = CalculateYawDegrees(directionToTargetXZ); + + // Step 7: Calculate relative angle + float relativeAngle = targetYawDegrees - cameraYawDegrees; + if (relativeAngle < 0) + relativeAngle += 360f; + + // Step 8: Map relative angle to ArrowDirection + return relativeAngle; + } + + private static float GetCameraYaw(Vector3 cameraPosition, Vector3 lookAtVector) + { + // Step 1: Calculate the direction vector from camera to LookAtPoint + Vector3 directionFacing = lookAtVector - cameraPosition; + + // Step 2: Project the directionFacing onto the XZ plane (ignore Y) + Vector3 directionFacingXZ = new Vector3(directionFacing.X, 0, directionFacing.Z); + + // Handle the case where the LookAtPoint is directly above or below the camera + if (directionFacingXZ.LengthSquared() < 1e-10f) + { + // Default to facing forward along the Z-axis if LookAtPoint is directly above or below + directionFacingXZ = new Vector3(0, 0, 1); + } + else + { + directionFacingXZ = Vector3.Normalize(directionFacingXZ); + } + + // Step 3: Calculate the camera's yaw angle based on directionFacingXZ + return (CalculateYawDegrees(directionFacingXZ)); + } + + private void ClearAllVfx() + { + foreach (var vfx in _poseVfx) + { + _vfxSpawnManager.DespawnObject(vfx.Value); + } + _poseVfx.Clear(); + } + + private async Task FilterEntriesAsync(Vector3 cameraPos, Vector3 cameraLookAt) + { + var previousPoses = _nearbyData.Keys.ToList(); + _nearbyData.Clear(); + + var ownLocation = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetMapData()).ConfigureAwait(false); + var player = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetPlayerCharacter()).ConfigureAwait(false); + var currentServer = player.CurrentWorld; + var playerPos = player.Position; + + var cameraYaw = GetCameraYaw(cameraPos, cameraLookAt); + + bool ignoreHousingLimits = _charaDataConfigService.Current.NearbyIgnoreHousingLimitations; + bool onlyCurrentServer = _charaDataConfigService.Current.NearbyOwnServerOnly; + bool showOwnData = _charaDataConfigService.Current.NearbyShowOwnData; + + // initial filter on name + foreach (var data in _metaInfoCache.Where(d => (string.IsNullOrWhiteSpace(UserNoteFilter) + || ((d.Key.Alias ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase) + || d.Key.UID.Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase) + || (_serverConfigurationManager.GetNoteForUid(UserNoteFilter) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)))) + .ToDictionary(k => k.Key, k => k.Value)) + { + // filter all poses based on territory, that always must be correct + foreach (var pose in data.Value.Where(v => v.HasPoses && v.HasWorldData && (showOwnData || !v.IsOwnData)) + .SelectMany(k => k.PoseExtended) + .Where(p => p.HasPoseData + && p.HasWorldData + && p.WorldData!.Value.LocationInfo.TerritoryId == ownLocation.TerritoryId) + .ToList()) + { + var poseLocation = pose.WorldData!.Value.LocationInfo; + + bool isInHousing = poseLocation.WardId != 0; + var distance = Vector3.Distance(playerPos, pose.Position); + if (distance > _charaDataConfigService.Current.NearbyDistanceFilter) continue; + + + bool addEntry = (!isInHousing && poseLocation.MapId == ownLocation.MapId + && (!onlyCurrentServer || poseLocation.ServerId == currentServer.RowId)) + || (isInHousing + && (((ignoreHousingLimits && !onlyCurrentServer) + || (ignoreHousingLimits && onlyCurrentServer) && poseLocation.ServerId == currentServer.RowId) + || poseLocation.ServerId == currentServer.RowId) + && ((poseLocation.HouseId == 0 && poseLocation.DivisionId == ownLocation.DivisionId + && (ignoreHousingLimits || poseLocation.WardId == ownLocation.WardId)) + || (poseLocation.HouseId > 0 + && (ignoreHousingLimits || (poseLocation.HouseId == ownLocation.HouseId && poseLocation.WardId == ownLocation.WardId && poseLocation.DivisionId == ownLocation.DivisionId && poseLocation.RoomId == ownLocation.RoomId))) + )); + + if (addEntry) + _nearbyData[pose] = new() { Direction = GetAngleToTarget(cameraPos, cameraYaw, pose.Position), Distance = distance }; + } + } + + if (_charaDataConfigService.Current.NearbyDrawWisps && !_dalamudUtilService.IsInGpose && !_dalamudUtilService.IsInCombatOrPerforming) + await _dalamudUtilService.RunOnFrameworkThread(() => ManageWispsNearby(previousPoses)).ConfigureAwait(false); + } + + private unsafe void HandleFrameworkUpdate() + { + if (_lastExecutionTime.AddSeconds(0.5) > DateTime.UtcNow) return; + _lastExecutionTime = DateTime.UtcNow; + if (!ComputeNearbyData && !_charaDataConfigService.Current.NearbyShowAlways) + { + if (_nearbyData.Any()) + _nearbyData.Clear(); + if (_poseVfx.Any()) + ClearAllVfx(); + return; + } + + if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose || _dalamudUtilService.IsInCombatOrPerforming) + ClearAllVfx(); + + var camera = CameraManager.Instance()->CurrentCamera; + Vector3 cameraPos = new(camera->Position.X, camera->Position.Y, camera->Position.Z); + Vector3 lookAt = new(camera->LookAtVector.X, camera->LookAtVector.Y, camera->LookAtVector.Z); + + if (_filterEntriesRunningTask?.IsCompleted ?? true && _dalamudUtilService.IsLoggedIn) + _filterEntriesRunningTask = FilterEntriesAsync(cameraPos, lookAt); + } + + private void ManageWispsNearby(List previousPoses) + { + foreach (var data in _nearbyData.Keys) + { + if (_poseVfx.TryGetValue(data, out var _)) continue; + + Guid? vfxGuid; + if (data.MetaInfo.IsOwnData) + { + vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f); + } + else + { + vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2); + } + if (vfxGuid != null) + { + _poseVfx[data] = vfxGuid.Value; + } + } + + foreach (var data in previousPoses.Except(_nearbyData.Keys)) + { + if (_poseVfx.Remove(data, out var guid)) + { + _vfxSpawnManager.DespawnObject(guid); + } + } + } +} diff --git a/MareSynchronos/Services/CharaData/MareCharaFileDataFactory.cs b/MareSynchronos/Services/CharaData/MareCharaFileDataFactory.cs new file mode 100644 index 0000000..d9a2429 --- /dev/null +++ b/MareSynchronos/Services/CharaData/MareCharaFileDataFactory.cs @@ -0,0 +1,20 @@ +using MareSynchronos.API.Data; +using MareSynchronos.FileCache; +using MareSynchronos.Services.CharaData.Models; + +namespace MareSynchronos.Services.CharaData; + +public sealed class MareCharaFileDataFactory +{ + private readonly FileCacheManager _fileCacheManager; + + public MareCharaFileDataFactory(FileCacheManager fileCacheManager) + { + _fileCacheManager = fileCacheManager; + } + + public MareCharaFileData Create(string description, CharacterData characterCacheDto) + { + return new MareCharaFileData(_fileCacheManager, description, characterCacheDto); + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/CharaData/Models/CharaDataExtendedUpdateDto.cs b/MareSynchronos/Services/CharaData/Models/CharaDataExtendedUpdateDto.cs new file mode 100644 index 0000000..c0774e2 --- /dev/null +++ b/MareSynchronos/Services/CharaData/Models/CharaDataExtendedUpdateDto.cs @@ -0,0 +1,362 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.CharaData; + +namespace MareSynchronos.Services.CharaData.Models; + +public sealed record CharaDataExtendedUpdateDto : CharaDataUpdateDto +{ + private readonly CharaDataFullDto _charaDataFullDto; + + public CharaDataExtendedUpdateDto(CharaDataUpdateDto dto, CharaDataFullDto charaDataFullDto) : base(dto) + { + _charaDataFullDto = charaDataFullDto; + _userList = charaDataFullDto.AllowedUsers.ToList(); + _groupList = charaDataFullDto.AllowedGroups.ToList(); + _poseList = charaDataFullDto.PoseData.Select(k => new PoseEntry(k.Id) + { + Description = k.Description, + PoseData = k.PoseData, + WorldData = k.WorldData + }).ToList(); + } + + public CharaDataUpdateDto BaseDto => new(Id) + { + AllowedUsers = AllowedUsers, + AllowedGroups = AllowedGroups, + AccessType = base.AccessType, + CustomizeData = base.CustomizeData, + Description = base.Description, + ExpiryDate = base.ExpiryDate, + FileGamePaths = base.FileGamePaths, + FileSwaps = base.FileSwaps, + GlamourerData = base.GlamourerData, + ShareType = base.ShareType, + ManipulationData = base.ManipulationData, + Poses = Poses + }; + + public new string ManipulationData + { + get + { + return base.ManipulationData ?? _charaDataFullDto.ManipulationData; + } + set + { + base.ManipulationData = value; + if (string.Equals(base.ManipulationData, _charaDataFullDto.ManipulationData, StringComparison.Ordinal)) + { + base.ManipulationData = null; + } + } + } + + public new string Description + { + get + { + return base.Description ?? _charaDataFullDto.Description; + } + set + { + base.Description = value; + if (string.Equals(base.Description, _charaDataFullDto.Description, StringComparison.Ordinal)) + { + base.Description = null; + } + } + } + + public new DateTime ExpiryDate + { + get + { + return base.ExpiryDate ?? _charaDataFullDto.ExpiryDate; + } + private set + { + base.ExpiryDate = value; + if (Equals(base.ExpiryDate, _charaDataFullDto.ExpiryDate)) + { + base.ExpiryDate = null; + } + } + } + + public new AccessTypeDto AccessType + { + get + { + return base.AccessType ?? _charaDataFullDto.AccessType; + } + set + { + base.AccessType = value; + if (AccessType == AccessTypeDto.Public && ShareType == ShareTypeDto.Shared) + { + ShareType = ShareTypeDto.Private; + } + + if (Equals(base.AccessType, _charaDataFullDto.AccessType)) + { + base.AccessType = null; + } + } + } + + public new ShareTypeDto ShareType + { + get + { + return base.ShareType ?? _charaDataFullDto.ShareType; + } + set + { + base.ShareType = value; + if (ShareType == ShareTypeDto.Shared && AccessType == AccessTypeDto.Public) + { + base.ShareType = ShareTypeDto.Private; + } + + if (Equals(base.ShareType, _charaDataFullDto.ShareType)) + { + base.ShareType = null; + } + } + } + + public new List? FileGamePaths + { + get + { + return base.FileGamePaths ?? _charaDataFullDto.FileGamePaths; + } + set + { + base.FileGamePaths = value; + if (!(base.FileGamePaths ?? []).Except(_charaDataFullDto.FileGamePaths).Any() + && !_charaDataFullDto.FileGamePaths.Except(base.FileGamePaths ?? []).Any()) + { + base.FileGamePaths = null; + } + } + } + + public new List? FileSwaps + { + get + { + return base.FileSwaps ?? _charaDataFullDto.FileSwaps; + } + set + { + base.FileSwaps = value; + if (!(base.FileSwaps ?? []).Except(_charaDataFullDto.FileSwaps).Any() + && !_charaDataFullDto.FileSwaps.Except(base.FileSwaps ?? []).Any()) + { + base.FileSwaps = null; + } + } + } + + public new string? GlamourerData + { + get + { + return base.GlamourerData ?? _charaDataFullDto.GlamourerData; + } + set + { + base.GlamourerData = value; + if (string.Equals(base.GlamourerData, _charaDataFullDto.GlamourerData, StringComparison.Ordinal)) + { + base.GlamourerData = null; + } + } + } + + public new string? CustomizeData + { + get + { + return base.CustomizeData ?? _charaDataFullDto.CustomizeData; + } + set + { + base.CustomizeData = value; + if (string.Equals(base.CustomizeData, _charaDataFullDto.CustomizeData, StringComparison.Ordinal)) + { + base.CustomizeData = null; + } + } + } + + public IEnumerable UserList => _userList; + private readonly List _userList; + + public IEnumerable GroupList => _groupList; + private readonly List _groupList; + + public IEnumerable PoseList => _poseList; + private readonly List _poseList; + + public void AddUserToList(string user) + { + _userList.Add(new(user, null)); + UpdateAllowedUsers(); + } + + public void AddGroupToList(string group) + { + _groupList.Add(new(group, null)); + UpdateAllowedGroups(); + } + + private void UpdateAllowedUsers() + { + AllowedUsers = [.. _userList.Select(u => u.UID)]; + if (!AllowedUsers.Except(_charaDataFullDto.AllowedUsers.Select(u => u.UID), StringComparer.Ordinal).Any() + && !_charaDataFullDto.AllowedUsers.Select(u => u.UID).Except(AllowedUsers, StringComparer.Ordinal).Any()) + { + AllowedUsers = null; + } + } + + private void UpdateAllowedGroups() + { + AllowedGroups = [.. _groupList.Select(u => u.GID)]; + if (!AllowedGroups.Except(_charaDataFullDto.AllowedGroups.Select(u => u.GID), StringComparer.Ordinal).Any() + && !_charaDataFullDto.AllowedGroups.Select(u => u.GID).Except(AllowedGroups, StringComparer.Ordinal).Any()) + { + AllowedGroups = null; + } + } + + public void RemoveUserFromList(string user) + { + _userList.RemoveAll(u => string.Equals(u.UID, user, StringComparison.Ordinal)); + UpdateAllowedUsers(); + } + + public void RemoveGroupFromList(string group) + { + _groupList.RemoveAll(u => string.Equals(u.GID, group, StringComparison.Ordinal)); + UpdateAllowedGroups(); + } + + public void AddPose() + { + _poseList.Add(new PoseEntry(null)); + UpdatePoseList(); + } + + public void RemovePose(PoseEntry entry) + { + if (entry.Id != null) + { + entry.Description = null; + entry.WorldData = null; + entry.PoseData = null; + } + else + { + _poseList.Remove(entry); + } + + UpdatePoseList(); + } + + public void UpdatePoseList() + { + Poses = [.. _poseList]; + if (!Poses.Except(_charaDataFullDto.PoseData).Any() && !_charaDataFullDto.PoseData.Except(Poses).Any()) + { + Poses = null; + } + } + + public void SetExpiry(bool expiring) + { + if (expiring) + { + var date = DateTime.UtcNow.AddDays(7); + SetExpiry(date.Year, date.Month, date.Day); + } + else + { + ExpiryDate = DateTime.MaxValue; + } + } + + public void SetExpiry(int year, int month, int day) + { + int daysInMonth = DateTime.DaysInMonth(year, month); + if (day > daysInMonth) day = 1; + ExpiryDate = new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc); + } + + internal void UndoChanges() + { + base.Description = null; + base.AccessType = null; + base.ShareType = null; + base.GlamourerData = null; + base.FileSwaps = null; + base.FileGamePaths = null; + base.CustomizeData = null; + base.ManipulationData = null; + AllowedUsers = null; + AllowedGroups = null; + Poses = null; + _poseList.Clear(); + _poseList.AddRange(_charaDataFullDto.PoseData.Select(k => new PoseEntry(k.Id) + { + Description = k.Description, + PoseData = k.PoseData, + WorldData = k.WorldData + })); + } + + internal void RevertDeletion(PoseEntry pose) + { + if (pose.Id == null) return; + var oldPose = _charaDataFullDto.PoseData.Find(p => p.Id == pose.Id); + if (oldPose == null) return; + pose.Description = oldPose.Description; + pose.PoseData = oldPose.PoseData; + pose.WorldData = oldPose.WorldData; + UpdatePoseList(); + } + + internal bool PoseHasChanges(PoseEntry pose) + { + if (pose.Id == null) return false; + var oldPose = _charaDataFullDto.PoseData.Find(p => p.Id == pose.Id); + if (oldPose == null) return false; + return !string.Equals(pose.Description, oldPose.Description, StringComparison.Ordinal) + || !string.Equals(pose.PoseData, oldPose.PoseData, StringComparison.Ordinal) + || pose.WorldData != oldPose.WorldData; + } + + public bool HasChanges => + base.Description != null + || base.ExpiryDate != null + || base.AccessType != null + || base.ShareType != null + || AllowedUsers != null + || AllowedGroups != null + || base.GlamourerData != null + || base.FileSwaps != null + || base.FileGamePaths != null + || base.CustomizeData != null + || base.ManipulationData != null + || Poses != null; + + public bool IsAppearanceEqual => + string.Equals(GlamourerData, _charaDataFullDto.GlamourerData, StringComparison.Ordinal) + && string.Equals(CustomizeData, _charaDataFullDto.CustomizeData, StringComparison.Ordinal) + && FileGamePaths == _charaDataFullDto.FileGamePaths + && FileSwaps == _charaDataFullDto.FileSwaps + && string.Equals(ManipulationData, _charaDataFullDto.ManipulationData, StringComparison.Ordinal); +} diff --git a/MareSynchronos/Services/CharaData/Models/CharaDataFullExtendedDto.cs b/MareSynchronos/Services/CharaData/Models/CharaDataFullExtendedDto.cs new file mode 100644 index 0000000..35bf813 --- /dev/null +++ b/MareSynchronos/Services/CharaData/Models/CharaDataFullExtendedDto.cs @@ -0,0 +1,18 @@ +using MareSynchronos.API.Dto.CharaData; +using System.Collections.ObjectModel; + +namespace MareSynchronos.Services.CharaData.Models; + +public sealed record CharaDataFullExtendedDto : CharaDataFullDto +{ + public CharaDataFullExtendedDto(CharaDataFullDto baseDto) : base(baseDto) + { + FullId = baseDto.Uploader.UID + ":" + baseDto.Id; + MissingFiles = new ReadOnlyCollection(baseDto.OriginalFiles.Except(baseDto.FileGamePaths).ToList()); + HasMissingFiles = MissingFiles.Any(); + } + + public string FullId { get; set; } + public bool HasMissingFiles { get; init; } + public IReadOnlyCollection MissingFiles { get; init; } +} diff --git a/MareSynchronos/Services/CharaData/Models/CharaDataMetaInfoExtendedDto.cs b/MareSynchronos/Services/CharaData/Models/CharaDataMetaInfoExtendedDto.cs new file mode 100644 index 0000000..763056b --- /dev/null +++ b/MareSynchronos/Services/CharaData/Models/CharaDataMetaInfoExtendedDto.cs @@ -0,0 +1,31 @@ +using MareSynchronos.API.Dto.CharaData; + +namespace MareSynchronos.Services.CharaData.Models; + +public sealed record CharaDataMetaInfoExtendedDto : CharaDataMetaInfoDto +{ + private CharaDataMetaInfoExtendedDto(CharaDataMetaInfoDto baseMeta) : base(baseMeta) + { + FullId = baseMeta.Uploader.UID + ":" + baseMeta.Id; + } + + public List PoseExtended { get; private set; } = []; + public bool HasPoses => PoseExtended.Count != 0; + public bool HasWorldData => PoseExtended.Exists(p => p.HasWorldData); + public bool IsOwnData { get; private set; } + public string FullId { get; private set; } + + public async static Task Create(CharaDataMetaInfoDto baseMeta, DalamudUtilService dalamudUtilService, bool isOwnData = false) + { + CharaDataMetaInfoExtendedDto newDto = new(baseMeta); + + foreach (var pose in newDto.PoseData) + { + newDto.PoseExtended.Add(await PoseEntryExtended.Create(pose, newDto, dalamudUtilService).ConfigureAwait(false)); + } + + newDto.IsOwnData = isOwnData; + + return newDto; + } +} diff --git a/MareSynchronos/Services/CharaData/Models/GposeLobbyUserData.cs b/MareSynchronos/Services/CharaData/Models/GposeLobbyUserData.cs new file mode 100644 index 0000000..ff12398 --- /dev/null +++ b/MareSynchronos/Services/CharaData/Models/GposeLobbyUserData.cs @@ -0,0 +1,174 @@ +using Dalamud.Utility; +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.Utils; +using System.Globalization; +using System.Numerics; +using System.Text; + +namespace MareSynchronos.Services.CharaData.Models; + +public sealed record GposeLobbyUserData(UserData UserData) +{ + public void Reset() + { + HasWorldDataUpdate = WorldData != null; + HasPoseDataUpdate = ApplicablePoseData != null; + SpawnedVfxId = null; + LastAppliedCharaDataDate = DateTime.MinValue; + } + + private WorldData? _worldData; + public WorldData? WorldData + { + get => _worldData; set + { + _worldData = value; + HasWorldDataUpdate = true; + } + } + + public bool HasWorldDataUpdate { get; set; } = false; + + private PoseData? _fullPoseData; + private PoseData? _deltaPoseData; + + public PoseData? FullPoseData + { + get => _fullPoseData; + set + { + _fullPoseData = value; + ApplicablePoseData = CombinePoseData(); + HasPoseDataUpdate = true; + } + } + + public PoseData? DeltaPoseData + { + get => _deltaPoseData; + set + { + _deltaPoseData = value; + ApplicablePoseData = CombinePoseData(); + HasPoseDataUpdate = true; + } + } + + public PoseData? ApplicablePoseData { get; private set; } + public bool HasPoseDataUpdate { get; set; } = false; + public Guid? SpawnedVfxId { get; set; } + public Vector3? LastWorldPosition { get; set; } + public Vector3? TargetWorldPosition { get; set; } + public DateTime? UpdateStart { get; set; } + private CharaDataDownloadDto? _charaData; + public CharaDataDownloadDto? CharaData + { + get => _charaData; set + { + _charaData = value; + LastUpdatedCharaData = _charaData?.UpdatedDate ?? DateTime.MaxValue; + } + } + + public DateTime LastUpdatedCharaData { get; private set; } = DateTime.MaxValue; + public DateTime LastAppliedCharaDataDate { get; set; } = DateTime.MinValue; + public nint Address { get; set; } + public string AssociatedCharaName { get; set; } = string.Empty; + + private PoseData? CombinePoseData() + { + if (DeltaPoseData == null && FullPoseData != null) return FullPoseData; + if (FullPoseData == null) return null; + + PoseData output = FullPoseData!.Value.DeepClone(); + PoseData delta = DeltaPoseData!.Value; + + foreach (var bone in FullPoseData!.Value.Bones) + { + if (!delta.Bones.TryGetValue(bone.Key, out var data)) continue; + if (!data.Exists) + { + output.Bones.Remove(bone.Key); + } + else + { + output.Bones[bone.Key] = data; + } + } + + foreach (var bone in FullPoseData!.Value.MainHand) + { + if (!delta.MainHand.TryGetValue(bone.Key, out var data)) continue; + if (!data.Exists) + { + output.MainHand.Remove(bone.Key); + } + else + { + output.MainHand[bone.Key] = data; + } + } + + foreach (var bone in FullPoseData!.Value.OffHand) + { + if (!delta.OffHand.TryGetValue(bone.Key, out var data)) continue; + if (!data.Exists) + { + output.OffHand.Remove(bone.Key); + } + else + { + output.OffHand[bone.Key] = data; + } + } + + return output; + } + + public string WorldDataDescriptor { get; private set; } = string.Empty; + public Vector2 MapCoordinates { get; private set; } + public Lumina.Excel.Sheets.Map Map { get; private set; } + public HandledCharaDataEntry? HandledChara { get; set; } + + public async Task SetWorldDataDescriptor(DalamudUtilService dalamudUtilService) + { + if (WorldData == null) + { + WorldDataDescriptor = "No World Data found"; + } + + var worldData = WorldData!.Value; + MapCoordinates = await dalamudUtilService.RunOnFrameworkThread(() => + MapUtil.WorldToMap(new Vector2(worldData.PositionX, worldData.PositionY), dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map)) + .ConfigureAwait(false); + Map = dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map; + + StringBuilder sb = new(); + sb.AppendLine("Server: " + dalamudUtilService.WorldData.Value[(ushort)worldData.LocationInfo.ServerId]); + sb.AppendLine("Territory: " + dalamudUtilService.TerritoryData.Value[worldData.LocationInfo.TerritoryId]); + sb.AppendLine("Map: " + dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].MapName); + + if (worldData.LocationInfo.WardId != 0) + sb.AppendLine("Ward #: " + worldData.LocationInfo.WardId); + if (worldData.LocationInfo.DivisionId != 0) + { + sb.AppendLine("Subdivision: " + worldData.LocationInfo.DivisionId switch + { + 1 => "No", + 2 => "Yes", + _ => "-" + }); + } + if (worldData.LocationInfo.HouseId != 0) + { + sb.AppendLine("House #: " + (worldData.LocationInfo.HouseId == 100 ? "Apartments" : worldData.LocationInfo.HouseId.ToString())); + } + if (worldData.LocationInfo.RoomId != 0) + { + sb.AppendLine("Apartment #: " + worldData.LocationInfo.RoomId); + } + sb.AppendLine("Coordinates: X: " + MapCoordinates.X.ToString("0.0", CultureInfo.InvariantCulture) + ", Y: " + MapCoordinates.Y.ToString("0.0", CultureInfo.InvariantCulture)); + WorldDataDescriptor = sb.ToString(); + } +} diff --git a/MareSynchronos/Services/CharaData/Models/HandledCharaDataEntry.cs b/MareSynchronos/Services/CharaData/Models/HandledCharaDataEntry.cs new file mode 100644 index 0000000..6b45b79 --- /dev/null +++ b/MareSynchronos/Services/CharaData/Models/HandledCharaDataEntry.cs @@ -0,0 +1,6 @@ +namespace MareSynchronos.Services.CharaData.Models; + +public sealed record HandledCharaDataEntry(string Name, bool IsSelf, Guid? CustomizePlus, CharaDataMetaInfoExtendedDto MetaInfo) +{ + public CharaDataMetaInfoExtendedDto MetaInfo { get; set; } = MetaInfo; +} diff --git a/MareSynchronos/Services/CharaData/Models/MareCharaFileData.cs b/MareSynchronos/Services/CharaData/Models/MareCharaFileData.cs new file mode 100644 index 0000000..0dde199 --- /dev/null +++ b/MareSynchronos/Services/CharaData/Models/MareCharaFileData.cs @@ -0,0 +1,70 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.FileCache; +using System.Text; +using System.Text.Json; + +namespace MareSynchronos.Services.CharaData.Models; + +public record MareCharaFileData +{ + public string Description { get; set; } = string.Empty; + public string GlamourerData { get; set; } = string.Empty; + public string CustomizePlusData { get; set; } = string.Empty; + public string ManipulationData { get; set; } = string.Empty; + public List Files { get; set; } = []; + public List FileSwaps { get; set; } = []; + + public MareCharaFileData() { } + public MareCharaFileData(FileCacheManager manager, string description, CharacterData dto) + { + Description = description; + + if (dto.GlamourerData.TryGetValue(ObjectKind.Player, out var glamourerData)) + { + GlamourerData = glamourerData; + } + + dto.CustomizePlusData.TryGetValue(ObjectKind.Player, out var customizePlusData); + CustomizePlusData = customizePlusData ?? string.Empty; + ManipulationData = dto.ManipulationData; + + if (dto.FileReplacements.TryGetValue(ObjectKind.Player, out var fileReplacements)) + { + var grouped = fileReplacements.GroupBy(f => f.Hash, StringComparer.OrdinalIgnoreCase); + + foreach (var file in grouped) + { + if (string.IsNullOrEmpty(file.Key)) + { + foreach (var item in file) + { + FileSwaps.Add(new FileSwap(item.GamePaths, item.FileSwapPath)); + } + } + else + { + var filePath = manager.GetFileCacheByHash(file.First().Hash)?.ResolvedFilepath; + if (filePath != null) + { + Files.Add(new FileData(file.SelectMany(f => f.GamePaths), (int)new FileInfo(filePath).Length, file.First().Hash)); + } + } + } + } + } + + public byte[] ToByteArray() + { + return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(this)); + } + + public static MareCharaFileData FromByteArray(byte[] data) + { + return JsonSerializer.Deserialize(Encoding.UTF8.GetString(data))!; + } + + public record FileSwap(IEnumerable GamePaths, string FileSwapPath); + + public record FileData(IEnumerable GamePaths, int Length, string Hash); +} \ No newline at end of file diff --git a/MareSynchronos/Services/CharaData/Models/MareCharaFileHeader.cs b/MareSynchronos/Services/CharaData/Models/MareCharaFileHeader.cs new file mode 100644 index 0000000..43f6ee5 --- /dev/null +++ b/MareSynchronos/Services/CharaData/Models/MareCharaFileHeader.cs @@ -0,0 +1,54 @@ +namespace MareSynchronos.Services.CharaData.Models; + +public record MareCharaFileHeader(byte Version, MareCharaFileData CharaFileData) +{ + public static readonly byte CurrentVersion = 1; + + public byte Version { get; set; } = Version; + public MareCharaFileData CharaFileData { get; set; } = CharaFileData; + public string FilePath { get; private set; } = string.Empty; + + public void WriteToStream(BinaryWriter writer) + { + writer.Write('M'); + writer.Write('C'); + writer.Write('D'); + writer.Write('F'); + writer.Write(Version); + var charaFileDataArray = CharaFileData.ToByteArray(); + writer.Write(charaFileDataArray.Length); + writer.Write(charaFileDataArray); + } + + public static MareCharaFileHeader? FromBinaryReader(string path, BinaryReader reader) + { + var chars = new string(reader.ReadChars(4)); + if (!string.Equals(chars, "MCDF", StringComparison.Ordinal)) throw new InvalidDataException("Not a Mare Chara File"); + + MareCharaFileHeader? decoded = null; + + var version = reader.ReadByte(); + if (version == 1) + { + var dataLength = reader.ReadInt32(); + + decoded = new(version, MareCharaFileData.FromByteArray(reader.ReadBytes(dataLength))) + { + FilePath = path, + }; + } + + return decoded; + } + + public static void AdvanceReaderToData(BinaryReader reader) + { + reader.ReadChars(4); + var version = reader.ReadByte(); + if (version == 1) + { + var length = reader.ReadInt32(); + _ = reader.ReadBytes(length); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/CharaData/Models/PoseEntryExtended.cs b/MareSynchronos/Services/CharaData/Models/PoseEntryExtended.cs new file mode 100644 index 0000000..c48cb2c --- /dev/null +++ b/MareSynchronos/Services/CharaData/Models/PoseEntryExtended.cs @@ -0,0 +1,75 @@ +using Dalamud.Utility; +using Lumina.Excel.Sheets; +using MareSynchronos.API.Dto.CharaData; +using System.Globalization; +using System.Numerics; +using System.Text; + +namespace MareSynchronos.Services.CharaData.Models; + +public sealed record PoseEntryExtended : PoseEntry +{ + private PoseEntryExtended(PoseEntry basePose, CharaDataMetaInfoExtendedDto parent) : base(basePose) + { + HasPoseData = !string.IsNullOrEmpty(basePose.PoseData); + HasWorldData = (WorldData ?? default) != default; + if (HasWorldData) + { + Position = new(basePose.WorldData!.Value.PositionX, basePose.WorldData!.Value.PositionY, basePose.WorldData!.Value.PositionZ); + Rotation = new(basePose.WorldData!.Value.RotationX, basePose.WorldData!.Value.RotationY, basePose.WorldData!.Value.RotationZ, basePose.WorldData!.Value.RotationW); + } + MetaInfo = parent; + } + + public CharaDataMetaInfoExtendedDto MetaInfo { get; } + public bool HasPoseData { get; } + public bool HasWorldData { get; } + public Vector3 Position { get; } = new(); + public Vector2 MapCoordinates { get; private set; } = new(); + public Quaternion Rotation { get; } = new(); + public Map Map { get; private set; } + public string WorldDataDescriptor { get; private set; } = string.Empty; + + public static async Task Create(PoseEntry baseEntry, CharaDataMetaInfoExtendedDto parent, DalamudUtilService dalamudUtilService) + { + PoseEntryExtended newPose = new(baseEntry, parent); + + if (newPose.HasWorldData) + { + var worldData = newPose.WorldData!.Value; + newPose.MapCoordinates = await dalamudUtilService.RunOnFrameworkThread(() => + MapUtil.WorldToMap(new Vector2(worldData.PositionX, worldData.PositionY), dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map)) + .ConfigureAwait(false); + newPose.Map = dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map; + + StringBuilder sb = new(); + sb.AppendLine("Server: " + dalamudUtilService.WorldData.Value[(ushort)worldData.LocationInfo.ServerId]); + sb.AppendLine("Territory: " + dalamudUtilService.TerritoryData.Value[worldData.LocationInfo.TerritoryId]); + sb.AppendLine("Map: " + dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].MapName); + + if (worldData.LocationInfo.WardId != 0) + sb.AppendLine("Ward #: " + worldData.LocationInfo.WardId); + if (worldData.LocationInfo.DivisionId != 0) + { + sb.AppendLine("Subdivision: " + worldData.LocationInfo.DivisionId switch + { + 1 => "No", + 2 => "Yes", + _ => "-" + }); + } + if (worldData.LocationInfo.HouseId != 0) + { + sb.AppendLine("House #: " + (worldData.LocationInfo.HouseId == 100 ? "Apartments" : worldData.LocationInfo.HouseId.ToString())); + } + if (worldData.LocationInfo.RoomId != 0) + { + sb.AppendLine("Apartment #: " + worldData.LocationInfo.RoomId); + } + sb.AppendLine("Coordinates: X: " + newPose.MapCoordinates.X.ToString("0.0", CultureInfo.InvariantCulture) + ", Y: " + newPose.MapCoordinates.Y.ToString("0.0", CultureInfo.InvariantCulture)); + newPose.WorldDataDescriptor = sb.ToString(); + } + + return newPose; + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/CharacterAnalyzer.cs b/MareSynchronos/Services/CharacterAnalyzer.cs new file mode 100644 index 0000000..26429d4 --- /dev/null +++ b/MareSynchronos/Services/CharacterAnalyzer.cs @@ -0,0 +1,242 @@ +using Lumina.Data.Files; +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.FileCache; +using MareSynchronos.Services.Mediator; +using MareSynchronos.UI; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase +{ + private readonly FileCacheManager _fileCacheManager; + private readonly XivDataAnalyzer _xivDataAnalyzer; + private CancellationTokenSource? _analysisCts; + private CancellationTokenSource _baseAnalysisCts = new(); + private string _lastDataHash = string.Empty; + + public CharacterAnalyzer(ILogger logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer) + : base(logger, mediator) + { + Mediator.Subscribe(this, (msg) => + { + _baseAnalysisCts = _baseAnalysisCts.CancelRecreate(); + var token = _baseAnalysisCts.Token; + _ = BaseAnalysis(msg.CharacterData, token); + }); + _fileCacheManager = fileCacheManager; + _xivDataAnalyzer = modelAnalyzer; + } + + public int CurrentFile { get; internal set; } + public bool IsAnalysisRunning => _analysisCts != null; + public int TotalFiles { get; internal set; } + internal Dictionary> LastAnalysis { get; } = []; + + 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(); + 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) + { + Logger.LogDebug("Computing file {file}", file.FilePaths[0]); + await file.ComputeSizes(_fileCacheManager, cancelToken, ignoreCacheEntries: true).ConfigureAwait(false); + CurrentFile++; + } + + _fileCacheManager.WriteOutFullCsv(); + + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to analyze files"); + } + finally + { + Mediator.Publish(new ResumeScanMessage(nameof(CharacterAnalyzer))); + } + } + + Mediator.Publish(new CharacterDataAnalyzedMessage()); + + _analysisCts.CancelDispose(); + _analysisCts = null; + + if (print) PrintAnalysis(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (!disposing) return; + + _analysisCts?.CancelDispose(); + _baseAnalysisCts.CancelDispose(); + } + + 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); + foreach (var fileEntry in obj.Value) + { + token.ThrowIfCancellationRequested(); + + var fileCacheEntries = _fileCacheManager.GetAllFileCachesByHash(fileEntry.Hash, ignoreCacheEntries: true, validate: false).ToList(); + if (fileCacheEntries.Count == 0) continue; + + var filePath = fileCacheEntries[0].ResolvedFilepath; + FileInfo fi = new(filePath); + string ext = "unk?"; + try + { + ext = fi.Extension[1..]; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Could not identify extension for {path}", filePath); + } + + var tris = await Task.Run(() => _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash)).ConfigureAwait(false); + + foreach (var entry in fileCacheEntries) + { + data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext, + [.. fileEntry.GamePaths], + fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal).ToList(), + entry.Size > 0 ? entry.Size.Value : 0, + entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0, + tris); + } + } + + LastAnalysis[obj.Key] = data; + } + + Mediator.Publish(new CharacterDataAnalyzedMessage()); + + _lastDataHash = charaData.DataHash.Value; + } + + private void PrintAnalysis() + { + if (LastAnalysis.Count == 0) return; + foreach (var kvp in LastAnalysis) + { + 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); + foreach (var path in entry.Value.GamePaths) + { + Logger.LogInformation(" Game Path: {path}", path); + } + if (entry.Value.FilePaths.Count > 1) Logger.LogInformation(" Multiple fitting files detected for {key}", entry.Key); + foreach (var filePath in entry.Value.FilePaths) + { + Logger.LogInformation(" File Path: {path}", filePath); + } + Logger.LogInformation(" Size: {size}, Compressed: {compressed}", UiSharedService.ByteToString(entry.Value.OriginalSize), + UiSharedService.ByteToString(entry.Value.CompressedSize)); + } + } + foreach (var kvp in LastAnalysis) + { + Logger.LogInformation("=== Detailed summary by file type for {obj} ===", kvp.Key); + foreach (var entry in kvp.Value.Select(v => v.Value).GroupBy(v => v.FileType, StringComparer.Ordinal)) + { + Logger.LogInformation("{ext} files: {count}, size extracted: {size}, size compressed: {sizeComp}", entry.Key, entry.Count(), + UiSharedService.ByteToString(entry.Sum(v => v.OriginalSize)), UiSharedService.ByteToString(entry.Sum(v => v.CompressedSize))); + } + Logger.LogInformation("=== Total summary for {obj} ===", kvp.Key); + 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), + UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.OriginalSize))), + UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize)))); + Logger.LogInformation("IMPORTANT NOTES:\n\r- For uploads 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; + public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token, bool ignoreCacheEntries = true) + { + var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false); + var normalSize = new FileInfo(FilePaths[0]).Length; + var entries = fileCacheManager.GetAllFileCachesByHash(Hash, ignoreCacheEntries: ignoreCacheEntries, validate: false); + foreach (var entry in entries) + { + entry.Size = normalSize; + entry.CompressedSize = compressedsize.Item2.LongLength; + } + OriginalSize = normalSize; + CompressedSize = compressedsize.Item2.LongLength; + } + public long OriginalSize { get; private set; } = OriginalSize; + public long CompressedSize { get; private set; } = CompressedSize; + public long Triangles { get; private set; } = Triangles; + + public Lazy Format = new(() => + { + switch (FileType) + { + 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(); + var width = reader.ReadInt16(); + var height = reader.ReadInt16(); + return $"{format} ({width}x{height})"; + } + catch + { + return "Unknown"; + } + } + default: + return string.Empty; + } + }); + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/ChatService.cs b/MareSynchronos/Services/ChatService.cs new file mode 100644 index 0000000..d3c399f --- /dev/null +++ b/MareSynchronos/Services/ChatService.cs @@ -0,0 +1,241 @@ +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Plugin.Services; +using MareSynchronos.API.Data; +using MareSynchronos.Interop; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public class ChatService : DisposableMediatorSubscriberBase +{ + public const int DefaultColor = 710; + public const int CommandMaxNumber = 50; + + private readonly ILogger _logger; + private readonly IChatGui _chatGui; + private readonly DalamudUtilService _dalamudUtil; + private readonly MareConfigService _mareConfig; + private readonly ApiController _apiController; + private readonly PairManager _pairManager; + private readonly ServerConfigurationManager _serverConfigurationManager; + + private readonly Lazy _gameChatHooks; + + public ChatService(ILogger logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController, + PairManager pairManager, ILoggerFactory loggerFactory, IGameInteropProvider gameInteropProvider, IChatGui chatGui, + MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator) + { + _logger = logger; + _dalamudUtil = dalamudUtil; + _chatGui = chatGui; + _mareConfig = mareConfig; + _apiController = apiController; + _pairManager = pairManager; + _serverConfigurationManager = serverConfigurationManager; + + Mediator.Subscribe(this, HandleUserChat); + Mediator.Subscribe(this, HandleGroupChat); + + _gameChatHooks = new(() => new GameChatHooks(loggerFactory.CreateLogger(), gameInteropProvider, SendChatShell)); + + // Initialize chat hooks in advance + _ = Task.Run(() => + { + try + { + _ = _gameChatHooks.Value; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to initialize chat hooks"); + } + }); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (_gameChatHooks.IsValueCreated) + _gameChatHooks.Value!.Dispose(); + } + + private void HandleUserChat(UserChatMsgMessage message) + { + var chatMsg = message.ChatMsg; + var prefix = new SeStringBuilder(); + prefix.AddText("[SnowChat] "); + _chatGui.Print(new XivChatEntry{ + MessageBytes = [..prefix.Build().Encode(), ..message.ChatMsg.PayloadContent], + Name = chatMsg.SenderName, + Type = XivChatType.TellIncoming + }); + } + + private ushort ResolveShellColor(int shellColor) + { + if (shellColor != 0) + return (ushort)shellColor; + var globalColor = _mareConfig.Current.ChatColor; + if (globalColor != 0) + return (ushort)globalColor; + return (ushort)DefaultColor; + } + + private XivChatType ResolveShellLogKind(int shellLogKind) + { + if (shellLogKind != 0) + return (XivChatType)shellLogKind; + return (XivChatType)_mareConfig.Current.ChatLogKind; + } + + private void HandleGroupChat(GroupChatMsgMessage message) + { + if (_mareConfig.Current.DisableSyncshellChat) + return; + + var chatMsg = message.ChatMsg; + var shellConfig = _serverConfigurationManager.GetShellConfigForGid(message.GroupInfo.GID); + var shellNumber = shellConfig.ShellNumber; + + if (!shellConfig.Enabled) + return; + + ushort color = ResolveShellColor(shellConfig.Color); + var extraChatTags = _mareConfig.Current.ExtraChatTags; + var logKind = ResolveShellLogKind(shellConfig.LogKind); + + var msg = new SeStringBuilder(); + if (extraChatTags) + { + msg.Add(ChatUtils.CreateExtraChatTagPayload(message.GroupInfo.GID)); + msg.Add(RawPayload.LinkTerminator); + } + if (color != 0) + msg.AddUiForeground((ushort)color); + msg.AddText($"[SS{shellNumber}]<"); + if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal)) + { + // Don't link to your own character + msg.AddText(chatMsg.SenderName); + } + else + { + msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId)); + } + msg.AddText("> "); + msg.Append(SeString.Parse(message.ChatMsg.PayloadContent)); + if (color != 0) + msg.AddUiForegroundOff(); + + _chatGui.Print(new XivChatEntry{ + Message = msg.Build(), + Name = chatMsg.SenderName, + Type = logKind + }); + } + + // Print an example message to the configured global chat channel + public void PrintChannelExample(string message, string gid = "") + { + int chatType = _mareConfig.Current.ChatLogKind; + + foreach (var group in _pairManager.Groups) + { + if (group.Key.GID.Equals(gid, StringComparison.Ordinal)) + { + int shellChatType = _serverConfigurationManager.GetShellConfigForGid(gid).LogKind; + if (shellChatType != 0) + chatType = shellChatType; + } + } + + _chatGui.Print(new XivChatEntry{ + Message = message, + Name = "", + Type = (XivChatType)chatType + }); + } + + // Called to update the active chat shell name if its renamed + public void MaybeUpdateShellName(int shellNumber) + { + if (_mareConfig.Current.DisableSyncshellChat) + return; + + foreach (var group in _pairManager.Groups) + { + var shellConfig = _serverConfigurationManager.GetShellConfigForGid(group.Key.GID); + if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) + { + if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null) + { + // Very dumb and won't handle re-numbering -- need to identify the active chat channel more reliably later + if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"SS [{shellNumber}]", StringComparison.Ordinal)) + SwitchChatShell(shellNumber); + } + } + } + } + + public void SwitchChatShell(int shellNumber) + { + if (_mareConfig.Current.DisableSyncshellChat) + return; + + foreach (var group in _pairManager.Groups) + { + var shellConfig = _serverConfigurationManager.GetShellConfigForGid(group.Key.GID); + if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) + { + var name = _serverConfigurationManager.GetNoteForGid(group.Key.GID) ?? group.Key.AliasOrGID; + // BUG: This doesn't always update the chat window e.g. when renaming a group + _gameChatHooks.Value.ChatChannelOverride = new() + { + ChannelName = $"SS [{shellNumber}]: {name}", + ChatMessageHandler = chatBytes => SendChatShell(shellNumber, chatBytes) + }; + return; + } + } + + _chatGui.PrintError($"[SnowcloakSync] Syncshell number #{shellNumber} not found"); + } + + public void SendChatShell(int shellNumber, byte[] chatBytes) + { + if (_mareConfig.Current.DisableSyncshellChat) + return; + + foreach (var group in _pairManager.Groups) + { + var shellConfig = _serverConfigurationManager.GetShellConfigForGid(group.Key.GID); + if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) + { + _ = Task.Run(async () => { + // Should cache the name and home world instead of fetching it every time + var chatMsg = await _dalamudUtil.RunOnFrameworkThread(() => { + return new ChatMessage() + { + SenderName = _dalamudUtil.GetPlayerName(), + SenderHomeWorldId = _dalamudUtil.GetHomeWorldId(), + PayloadContent = chatBytes + }; + }).ConfigureAwait(false); + await _apiController.GroupChatSendMsg(new(group.Key), chatMsg).ConfigureAwait(false); + }).ConfigureAwait(false); + return; + } + } + + _chatGui.PrintError($"[SnowcloakSync] Syncshell number #{shellNumber} not found"); + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/CommandManagerService.cs b/MareSynchronos/Services/CommandManagerService.cs new file mode 100644 index 0000000..3676a0e --- /dev/null +++ b/MareSynchronos/Services/CommandManagerService.cs @@ -0,0 +1,155 @@ +using Dalamud.Game.Command; +using Dalamud.Plugin.Services; +using MareSynchronos.FileCache; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI; +using MareSynchronos.WebAPI; +using System.Globalization; +using System.Text; + +namespace MareSynchronos.Services; + +public sealed class CommandManagerService : IDisposable +{ + private const string _commandName = "/sync"; + private const string _commandName2 = "/snowcloak"; + + private const string _ssCommandPrefix = "/ss"; + + private readonly ApiController _apiController; + private readonly ICommandManager _commandManager; + private readonly MareMediator _mediator; + private readonly MareConfigService _mareConfigService; + private readonly PerformanceCollectorService _performanceCollectorService; + private readonly CacheMonitor _cacheMonitor; + private readonly ChatService _chatService; + private readonly ServerConfigurationManager _serverConfigurationManager; + + public CommandManagerService(ICommandManager commandManager, PerformanceCollectorService performanceCollectorService, + ServerConfigurationManager serverConfigurationManager, CacheMonitor periodicFileScanner, ChatService chatService, + ApiController apiController, MareMediator mediator, MareConfigService mareConfigService) + { + _commandManager = commandManager; + _performanceCollectorService = performanceCollectorService; + _serverConfigurationManager = serverConfigurationManager; + _cacheMonitor = periodicFileScanner; + _chatService = chatService; + _apiController = apiController; + _mediator = mediator; + _mareConfigService = mareConfigService; + _commandManager.AddHandler(_commandName, new CommandInfo(OnCommand) + { + HelpMessage = "Opens the Snowcloak UI" + }); + _commandManager.AddHandler(_commandName2, new CommandInfo(OnCommand) + { + HelpMessage = "Opens the Snowcloak UI" + }); + + // Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway + for (int i = 1; i <= ChatService.CommandMaxNumber; ++i) + { + _commandManager.AddHandler($"{_ssCommandPrefix}{i}", new CommandInfo(OnChatCommand) + { + ShowInHelp = false + }); + } + } + + public void Dispose() + { + _commandManager.RemoveHandler(_commandName); + _commandManager.RemoveHandler(_commandName2); + + for (int i = 1; i <= ChatService.CommandMaxNumber; ++i) + _commandManager.RemoveHandler($"{_ssCommandPrefix}{i}"); + } + + private void OnCommand(string command, string args) + { + var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries); + + if (splitArgs.Length == 0) + { + // Interpret this as toggling the UI + if (_mareConfigService.Current.HasValidSetup()) + _mediator.Publish(new UiToggleMessage(typeof(CompactUi))); + else + _mediator.Publish(new UiToggleMessage(typeof(IntroUi))); + return; + } + + if (string.Equals(splitArgs[0], "toggle", StringComparison.OrdinalIgnoreCase)) + { + if (_apiController.ServerState == WebAPI.SignalR.Utils.ServerState.Disconnecting) + { + _mediator.Publish(new NotificationMessage("Snowcloak disconnecting", "Cannot use /toggle while Snowcloak is still disconnecting", + NotificationType.Error)); + } + + if (_serverConfigurationManager.CurrentServer == null) return; + var fullPause = splitArgs.Length > 1 ? splitArgs[1] switch + { + "on" => false, + "off" => true, + _ => !_serverConfigurationManager.CurrentServer.FullPause, + } : !_serverConfigurationManager.CurrentServer.FullPause; + + if (fullPause != _serverConfigurationManager.CurrentServer.FullPause) + { + _serverConfigurationManager.CurrentServer.FullPause = fullPause; + _serverConfigurationManager.Save(); + _ = _apiController.CreateConnections(); + } + } + else if (string.Equals(splitArgs[0], "gpose", StringComparison.OrdinalIgnoreCase)) + { + _mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi))); + } + else if (string.Equals(splitArgs[0], "rescan", StringComparison.OrdinalIgnoreCase)) + { + _cacheMonitor.InvokeScan(); + } + else if (string.Equals(splitArgs[0], "perf", StringComparison.OrdinalIgnoreCase)) + { + if (splitArgs.Length > 1 && int.TryParse(splitArgs[1], CultureInfo.InvariantCulture, out var limitBySeconds)) + { + _performanceCollectorService.PrintPerformanceStats(limitBySeconds); + } + else + { + _performanceCollectorService.PrintPerformanceStats(); + } + } + else if (string.Equals(splitArgs[0], "medi", StringComparison.OrdinalIgnoreCase)) + { + _mediator.PrintSubscriberInfo(); + } + else if (string.Equals(splitArgs[0], "analyze", StringComparison.OrdinalIgnoreCase)) + { + _mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); + } + } + + private void OnChatCommand(string command, string args) + { + if (_mareConfigService.Current.DisableSyncshellChat) + return; + + int shellNumber = int.Parse(command[_ssCommandPrefix.Length..]); + + if (args.Length == 0) + { + _chatService.SwitchChatShell(shellNumber); + } + else + { + // FIXME: Chat content seems to already be stripped of any special characters here? + byte[] chatBytes = Encoding.UTF8.GetBytes(args); + _chatService.SendChatShell(shellNumber, chatBytes); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/DalamudUtilService.cs b/MareSynchronos/Services/DalamudUtilService.cs new file mode 100644 index 0000000..aaa8d87 --- /dev/null +++ b/MareSynchronos/Services/DalamudUtilService.cs @@ -0,0 +1,794 @@ +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin.Services; +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using Lumina.Excel.Sheets; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.Interop; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; +using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; +using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.IGameObject; + +namespace MareSynchronos.Services; + +public class DalamudUtilService : IHostedService, IMediatorSubscriber +{ + public struct PlayerCharacter + { + public uint ObjectId; + public string Name; + public uint HomeWorldId; + public nint Address; + }; + + private struct PlayerInfo + { + public PlayerCharacter Character; + public string Hash; + }; + + private readonly List _classJobIdsIgnoredForPets = [30]; + private readonly IClientState _clientState; + private readonly ICondition _condition; + private readonly IDataManager _gameData; + private readonly BlockedCharacterHandler _blockedCharacterHandler; + private readonly IFramework _framework; + private readonly IGameGui _gameGui; + private readonly IToastGui _toastGui; + private readonly ILogger _logger; + private readonly IObjectTable _objectTable; + private readonly PerformanceCollectorService _performanceCollector; + 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 bool _sentBetweenAreas = false; + private static readonly Dictionary _playerInfoCache = new(); + + + public DalamudUtilService(ILogger logger, IClientState clientState, IObjectTable objectTable, IFramework framework, + IGameGui gameGui, IToastGui toastGui,ICondition condition, IDataManager gameData, ITargetManager targetManager, + BlockedCharacterHandler blockedCharacterHandler, MareMediator mediator, PerformanceCollectorService performanceCollector) + { + _logger = logger; + _clientState = clientState; + _objectTable = objectTable; + _framework = framework; + _gameGui = gameGui; + _toastGui = toastGui; + _condition = condition; + _gameData = gameData; + _blockedCharacterHandler = blockedCharacterHandler; + Mediator = mediator; + _performanceCollector = performanceCollector; + WorldData = new(() => + { + return gameData.GetExcelSheet(Dalamud.Game.ClientLanguage.English)! + .Where(w => w.Name.ByteLength > 0 && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper((char)w.Name.Data.Span[0]))) + .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); + }); + UiColors = new(() => + { + return gameData.GetExcelSheet(Dalamud.Game.ClientLanguage.English)! + .Where(x => x.RowId != 0 && !(x.RowId >= 500 && (x.Dark & 0xFFFFFF00) == 0)) + .ToDictionary(x => (int)x.RowId); + }); + TerritoryData = new(() => + { + return gameData.GetExcelSheet(Dalamud.Game.ClientLanguage.English)! + .Where(w => w.RowId != 0) + .ToDictionary(w => w.RowId, w => + { + StringBuilder sb = new(); + sb.Append(w.PlaceNameRegion.Value.Name); + if (w.PlaceName.ValueNullable != null) + { + sb.Append(" - "); + sb.Append(w.PlaceName.Value.Name); + } + return sb.ToString(); + }); + }); + MapData = new(() => + { + return gameData.GetExcelSheet(Dalamud.Game.ClientLanguage.English)! + .Where(w => w.RowId != 0) + .ToDictionary(w => w.RowId, w => + { + StringBuilder sb = new(); + sb.Append(w.PlaceNameRegion.Value.Name); + if (w.PlaceName.ValueNullable != null) + { + sb.Append(" - "); + sb.Append(w.PlaceName.Value.Name); + } + if (w.PlaceNameSub.ValueNullable != null && !string.IsNullOrEmpty(w.PlaceNameSub.Value.Name.ToString())) + { + sb.Append(" - "); + sb.Append(w.PlaceNameSub.Value.Name); + } + return (w, sb.ToString()); + }); + }); + mediator.Subscribe(this, (msg) => + { + if (clientState.IsPvP) return; + var ident = msg.Pair.GetPlayerNameHash(); + _ = RunOnFrameworkThread(() => + { + var addr = GetPlayerCharacterFromCachedTableByIdent(ident); + var pc = _clientState.LocalPlayer!; + var gobj = CreateGameObject(addr); + // Any further than roughly 55y is out of range for targetting + if (gobj != null && Vector3.Distance(pc.Position, gobj.Position) < 55.0f) + targetManager.Target = gobj; + else + _toastGui.ShowError("Player out of range."); + }).ConfigureAwait(false); + }); + IsWine = Util.IsWine(); + } + + public bool IsWine { get; init; } + public unsafe GameObject* GposeTarget + { + get => TargetSystem.Instance()->GPoseTarget; + set => TargetSystem.Instance()->GPoseTarget = value; + } + + private unsafe bool HasGposeTarget => GposeTarget != null; + private unsafe int GPoseTargetIdx => !HasGposeTarget ? -1 : GposeTarget->ObjectIndex; + + public async Task GetGposeTargetGameObjectAsync() + { + if (!HasGposeTarget) + return null; + + return await _framework.RunOnFrameworkThread(() => _objectTable[GPoseTargetIdx]).ConfigureAwait(true); + } + public bool IsAnythingDrawing { get; private set; } = false; + public bool IsInCutscene { get; private set; } = false; + public bool IsInGpose { get; private set; } = false; + public bool IsLoggedIn { get; private set; } + public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread; + public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]; + public bool IsInCombatOrPerforming { get; private set; } = false; + public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles; + + public Lazy> WorldData { get; private set; } + public Lazy> UiColors { get; private set; } + public Lazy> TerritoryData { get; private set; } + public Lazy> MapData { get; private set; } + + public MareMediator Mediator { get; } + + public Dalamud.Game.ClientState.Objects.Types.IGameObject? CreateGameObject(IntPtr reference) + { + EnsureIsOnFramework(); + return _objectTable.CreateObjectReference(reference); + } + + public async Task CreateGameObjectAsync(IntPtr reference) + { + return await RunOnFrameworkThread(() => _objectTable.CreateObjectReference(reference)).ConfigureAwait(false); + } + + public void EnsureIsOnFramework() + { + if (!_framework.IsInFrameworkUpdateThread) throw new InvalidOperationException("Can only be run on Framework"); + } + + public Dalamud.Game.ClientState.Objects.Types.ICharacter? GetCharacterFromObjectTableByIndex(int index) + { + EnsureIsOnFramework(); + var objTableObj = _objectTable[index]; + if (objTableObj!.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) return null; + return (Dalamud.Game.ClientState.Objects.Types.ICharacter)objTableObj; + } + + public unsafe IntPtr GetCompanion(IntPtr? playerPointer = null) + { + EnsureIsOnFramework(); + var mgr = CharacterManager.Instance(); + playerPointer ??= GetPlayerPointer(); + if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero; + return (IntPtr)mgr->LookupBuddyByOwnerObject((BattleChara*)playerPointer); + } + + public async Task GetCompanionAsync(IntPtr? playerPointer = null) + { + return await RunOnFrameworkThread(() => GetCompanion(playerPointer)).ConfigureAwait(false); + } + + public async Task GetGposeCharacterFromObjectTableByNameAsync(string name, bool onlyGposeCharacters = false) + { + return await RunOnFrameworkThread(() => GetGposeCharacterFromObjectTableByName(name, onlyGposeCharacters)).ConfigureAwait(false); + } + + public ICharacter? GetGposeCharacterFromObjectTableByName(string name, bool onlyGposeCharacters = false) + { + EnsureIsOnFramework(); + return (ICharacter?)_objectTable + .FirstOrDefault(i => (!onlyGposeCharacters || i.ObjectIndex >= 200) && string.Equals(i.Name.ToString(), name, StringComparison.Ordinal)); + } + + public IEnumerable GetGposeCharactersFromObjectTable() + { + return _objectTable.Where(o => o.ObjectIndex > 200 && o.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player).Cast(); + } + + public bool GetIsPlayerPresent() + { + EnsureIsOnFramework(); + return _clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid(); + } + + public async Task GetIsPlayerPresentAsync() + { + return await RunOnFrameworkThread(GetIsPlayerPresent).ConfigureAwait(false); + } + + public unsafe IntPtr GetMinionOrMount(IntPtr? playerPointer = null) + { + EnsureIsOnFramework(); + playerPointer ??= GetPlayerPointer(); + if (playerPointer == IntPtr.Zero) return IntPtr.Zero; + return _objectTable.GetObjectAddress(((GameObject*)playerPointer)->ObjectIndex + 1); + } + + public async Task GetMinionOrMountAsync(IntPtr? playerPointer = null) + { + return await RunOnFrameworkThread(() => GetMinionOrMount(playerPointer)).ConfigureAwait(false); + } + + public unsafe IntPtr GetPet(IntPtr? playerPointer = null) + { + EnsureIsOnFramework(); + if (_classJobIdsIgnoredForPets.Contains(_classJobId ?? 0)) return IntPtr.Zero; + var mgr = CharacterManager.Instance(); + playerPointer ??= GetPlayerPointer(); + if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero; + return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer); + } + + public async Task GetPetAsync(IntPtr? playerPointer = null) + { + return await RunOnFrameworkThread(() => GetPet(playerPointer)).ConfigureAwait(false); + } + + public async Task GetPlayerCharacterAsync() + { + return await RunOnFrameworkThread(GetPlayerCharacter).ConfigureAwait(false); + } + + public IPlayerCharacter GetPlayerCharacter() + { + EnsureIsOnFramework(); + return _clientState.LocalPlayer!; + } + + public IntPtr GetPlayerCharacterFromCachedTableByName(string characterName) + { + foreach (var c in _playerCharas.Values) + { + if (c.Name.Equals(characterName, StringComparison.Ordinal)) + return c.Address; + } + return 0; + } + + public IntPtr GetPlayerCharacterFromCachedTableByIdent(string characterName) + { + if (_playerCharas.TryGetValue(characterName, out var pchar)) return pchar.Address; + return IntPtr.Zero; + } + + public string GetPlayerName() + { + EnsureIsOnFramework(); + return _clientState.LocalPlayer?.Name.ToString() ?? "--"; + } + + public async Task GetPlayerNameAsync() + { + return await RunOnFrameworkThread(GetPlayerName).ConfigureAwait(false); + } + + public async Task GetPlayerNameHashedAsync() + { + return await RunOnFrameworkThread(() => (GetPlayerName() + GetHomeWorldId()).GetHash256()).ConfigureAwait(false); + } + + public IntPtr GetPlayerPointer() + { + EnsureIsOnFramework(); + return _clientState.LocalPlayer?.Address ?? IntPtr.Zero; + } + + public async Task GetPlayerPointerAsync() + { + return await RunOnFrameworkThread(GetPlayerPointer).ConfigureAwait(false); + } + + public uint GetHomeWorldId() + { + EnsureIsOnFramework(); + return _clientState.LocalPlayer!.HomeWorld.RowId; + } + + public uint GetWorldId() + { + EnsureIsOnFramework(); + return _clientState.LocalPlayer!.CurrentWorld.RowId; + } + + public unsafe LocationInfo GetMapData() + { + EnsureIsOnFramework(); + var agentMap = AgentMap.Instance(); + var houseMan = HousingManager.Instance(); + uint serverId = 0; + if (_clientState.LocalPlayer == null) serverId = 0; + else serverId = _clientState.LocalPlayer.CurrentWorld.RowId; + uint mapId = agentMap == null ? 0 : agentMap->CurrentMapId; + uint territoryId = agentMap == null ? 0 : agentMap->CurrentTerritoryId; + uint divisionId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentDivision()); + uint wardId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentWard() + 1); + uint houseId = 0; + var tempHouseId = houseMan == null ? 0 : (houseMan->GetCurrentPlot()); + if (!houseMan->IsInside()) tempHouseId = 0; + if (tempHouseId < -1) + { + divisionId = tempHouseId == -127 ? 2 : (uint)1; + tempHouseId = 100; + } + if (tempHouseId == -1) tempHouseId = 0; + houseId = (uint)tempHouseId; + if (houseId != 0) + { + territoryId = HousingManager.GetOriginalHouseTerritoryTypeId(); + } + uint roomId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentRoom()); + + return new LocationInfo() + { + ServerId = serverId, + MapId = mapId, + TerritoryId = territoryId, + DivisionId = divisionId, + WardId = wardId, + HouseId = houseId, + RoomId = roomId + }; + } + + public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map) + { + EnsureIsOnFramework(); + var agentMap = AgentMap.Instance(); + if (agentMap == null) return; + agentMap->OpenMapByMapId(map.RowId); + agentMap->SetFlagMapMarker(map.TerritoryType.RowId, map.RowId, position); + } + + public async Task GetMapDataAsync() + { + return await RunOnFrameworkThread(GetMapData).ConfigureAwait(false); + } + + public async Task GetWorldIdAsync() + { + return await RunOnFrameworkThread(GetWorldId).ConfigureAwait(false); + } + + public async Task GetHomeWorldIdAsync() + { + return await RunOnFrameworkThread(GetHomeWorldId).ConfigureAwait(false); + } + + public unsafe bool IsGameObjectPresent(IntPtr key) + { + return _objectTable.Any(f => f.Address == key); + } + + public bool IsObjectPresent(Dalamud.Game.ClientState.Objects.Types.IGameObject? obj) + { + EnsureIsOnFramework(); + return obj != null && obj.IsValid(); + } + + public async Task IsObjectPresentAsync(Dalamud.Game.ClientState.Objects.Types.IGameObject? obj) + { + return await RunOnFrameworkThread(() => IsObjectPresent(obj)).ConfigureAwait(false); + } + + public async Task RunOnFrameworkThread(System.Action act, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0) + { + var fileName = Path.GetFileNameWithoutExtension(callerFilePath); + await _performanceCollector.LogPerformance(this, $"RunOnFramework:Act/{fileName}>{callerMember}:{callerLineNumber}", async () => + { + 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(); + }).ConfigureAwait(false); + } + + public async Task RunOnFrameworkThread(Func func, [CallerMemberName] string callerMember = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0) + { + var fileName = Path.GetFileNameWithoutExtension(callerFilePath); + return await _performanceCollector.LogPerformance(this, $"RunOnFramework:Func<{typeof(T)}>/{fileName}>{callerMember}:{callerLineNumber}", async () => + { + 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(); + }).ConfigureAwait(false); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting DalamudUtilService"); +#pragma warning disable S2696 // Instance members should not write to "static" fields + Snowcloak.Plugin.Self.RealOnFrameworkUpdate = this.FrameworkOnUpdate; +#pragma warning restore S2696 + _framework.Update += Snowcloak.Plugin.Self.OnFrameworkUpdate; + if (IsLoggedIn) + { + _classJobId = _clientState.LocalPlayer!.ClassJob.RowId; + } + + _logger.LogInformation("Started DalamudUtilService"); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogTrace("Stopping {type}", GetType()); + + Mediator.UnsubscribeAll(this); + _framework.Update -= Snowcloak.Plugin.Self.OnFrameworkUpdate; + return Task.CompletedTask; + } + + public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null) + { + if (!_clientState.IsLoggedIn) return; + + const int tick = 250; + int curWaitTime = 0; + try + { + logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler); + await Task.Delay(tick).ConfigureAwait(true); + curWaitTime += tick; + + while ((!ct?.IsCancellationRequested ?? true) + && curWaitTime < timeOut + && await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) // 0b100000000000 is "still rendering" or something + { + logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler); + curWaitTime += tick; + await Task.Delay(tick).ConfigureAwait(true); + } + + logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime); + } + catch (NullReferenceException ex) + { + logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler); + } + catch (AccessViolationException ex) + { + logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler); + } + } + + public unsafe void WaitWhileGposeCharacterIsDrawing(IntPtr characterAddress, int timeOut = 5000) + { + Thread.Sleep(500); + var obj = (GameObject*)characterAddress; + const int tick = 250; + int curWaitTime = 0; + _logger.LogTrace("RenderFlags: {flags}", obj->RenderFlags.ToString("X")); + while (obj->RenderFlags != 0x00 && curWaitTime < timeOut) + { + _logger.LogTrace($"Waiting for gpose actor to finish drawing"); + curWaitTime += tick; + Thread.Sleep(tick); + } + + Thread.Sleep(tick * 2); + } + + public Vector2 WorldToScreen(Dalamud.Game.ClientState.Objects.Types.IGameObject? obj) + { + if (obj == null) return Vector2.Zero; + return _gameGui.WorldToScreen(obj.Position, out var screenPos) ? screenPos : Vector2.Zero; + } + + public PlayerCharacter FindPlayerByNameHash(string ident) + { + _playerCharas.TryGetValue(ident, out var result); + return result; + } + + private unsafe PlayerInfo GetPlayerInfo(DalamudGameObject chara) + { + uint id = chara.EntityId; + + if (!_playerInfoCache.TryGetValue(id, out var info)) + { + info.Character.ObjectId = id; + info.Character.Name = chara.Name.TextValue; // ? + info.Character.HomeWorldId = ((BattleChara*)chara.Address)->Character.HomeWorld; + info.Character.Address = chara.Address; + info.Hash = Crypto.GetHash256(info.Character.Name + info.Character.HomeWorldId.ToString()); + _playerInfoCache[id] = info; + } + + info.Character.Address = chara.Address; + + return info; + } + + private unsafe void CheckCharacterForDrawing(PlayerCharacter p) + { + var gameObj = (GameObject*)p.Address; + var drawObj = gameObj->DrawObject; + var characterName = p.Name; + bool isDrawing = false; + bool isDrawingChanged = false; + if ((nint)drawObj != IntPtr.Zero) + { + isDrawing = gameObj->RenderFlags == 0b100000000000; + if (!isDrawing) + { + isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0; + if (!isDrawing) + { + isDrawing = ((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0; + if (isDrawing && !string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal) + && !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal)) + { + _lastGlobalBlockPlayer = characterName; + _lastGlobalBlockReason = "HasModelFilesInSlotLoaded"; + isDrawingChanged = true; + } + } + else + { + if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal) + && !string.Equals(_lastGlobalBlockReason, "HasModelInSlotLoaded", StringComparison.Ordinal)) + { + _lastGlobalBlockPlayer = characterName; + _lastGlobalBlockReason = "HasModelInSlotLoaded"; + isDrawingChanged = true; + } + } + } + else + { + if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal) + && !string.Equals(_lastGlobalBlockReason, "RenderFlags", StringComparison.Ordinal)) + { + _lastGlobalBlockPlayer = characterName; + _lastGlobalBlockReason = "RenderFlags"; + isDrawingChanged = true; + } + } + } + + if (isDrawingChanged) + { + _logger.LogTrace("Global draw block: START => {name} ({reason})", characterName, _lastGlobalBlockReason); + } + + IsAnythingDrawing |= isDrawing; + } + + private void FrameworkOnUpdate(IFramework framework) + { + _performanceCollector.LogPerformance(this, $"FrameworkOnUpdate", FrameworkOnUpdateInternal); + } + + private unsafe void FrameworkOnUpdateInternal() + { + if (_clientState.LocalPlayer?.IsDead ?? false) + { + return; + } + + bool isNormalFrameworkUpdate = DateTime.UtcNow < _delayedFrameworkUpdateCheck.AddMilliseconds(200); + + _performanceCollector.LogPerformance(this, $"FrameworkOnUpdateInternal+{(isNormalFrameworkUpdate ? "Regular" : "Delayed")}", () => + { + IsAnythingDrawing = false; + _performanceCollector.LogPerformance(this, $"ObjTableToCharas", + () => + { + if (_sentBetweenAreas) + return; + + _notUpdatedCharas.AddRange(_playerCharas.Keys); + + for (int i = 0; i < 200; i += 2) + { + var chara = _objectTable[i]; + if (chara == null || chara.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) + continue; + + if (_blockedCharacterHandler.IsCharacterBlocked(chara.Address, out bool firstTime) && firstTime) + { + _logger.LogTrace("Skipping character {addr}, blocked/muted", chara.Address.ToString("X")); + continue; + } + + var info = GetPlayerInfo(chara); + + if (!IsAnythingDrawing) + CheckCharacterForDrawing(info.Character); + _notUpdatedCharas.Remove(info.Hash); + _playerCharas[info.Hash] = info.Character; + } + + foreach (var notUpdatedChara in _notUpdatedCharas) + { + _playerCharas.Remove(notUpdatedChara); + } + + _notUpdatedCharas.Clear(); + }); + + if (!IsAnythingDrawing && !string.IsNullOrEmpty(_lastGlobalBlockPlayer)) + { + _logger.LogTrace("Global draw block: END => {name}", _lastGlobalBlockPlayer); + _lastGlobalBlockPlayer = string.Empty; + _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()); + } + + if ((_condition[ConditionFlag.Performing] || _condition[ConditionFlag.InCombat]) && !IsInCombatOrPerforming) + { + _logger.LogDebug("Combat/Performance start"); + IsInCombatOrPerforming = true; + Mediator.Publish(new CombatOrPerformanceStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(IsInCombatOrPerforming))); + } + else if ((!_condition[ConditionFlag.Performing] && !_condition[ConditionFlag.InCombat]) && IsInCombatOrPerforming) + { + _logger.LogDebug("Combat/Performance end"); + IsInCombatOrPerforming = false; + Mediator.Publish(new CombatOrPerformanceEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(IsInCombatOrPerforming))); + } + + if (_condition[ConditionFlag.WatchingCutscene] && !IsInCutscene) + { + _logger.LogDebug("Cutscene start"); + IsInCutscene = true; + Mediator.Publish(new CutsceneStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(IsInCutscene))); + } + else if (!_condition[ConditionFlag.WatchingCutscene] && IsInCutscene) + { + _logger.LogDebug("Cutscene end"); + IsInCutscene = false; + Mediator.Publish(new CutsceneEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(IsInCutscene))); + } + + if (IsInCutscene) + { + Mediator.Publish(new CutsceneFrameworkUpdateMessage()); + return; + } + + if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]) + { + var zone = _clientState.TerritoryType; + if (_lastZone != zone) + { + _lastZone = zone; + if (!_sentBetweenAreas) + { + _logger.LogDebug("Zone switch/Gpose start"); + _sentBetweenAreas = true; + _playerInfoCache.Clear(); + Mediator.Publish(new ZoneSwitchStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(ConditionFlag.BetweenAreas))); + } + } + + return; + } + + if (_sentBetweenAreas) + { + _logger.LogDebug("Zone switch/Gpose end"); + _sentBetweenAreas = false; + Mediator.Publish(new ZoneSwitchEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas))); + } + + var localPlayer = _clientState.LocalPlayer; + if (localPlayer != null) + { + _classJobId = localPlayer.ClassJob.RowId; + } + + Mediator.Publish(new PriorityFrameworkUpdateMessage()); + + if (!IsInCombatOrPerforming) + Mediator.Publish(new FrameworkUpdateMessage()); + + if (isNormalFrameworkUpdate) + return; + + if (localPlayer != null && !IsLoggedIn) + { + _logger.LogDebug("Logged in"); + IsLoggedIn = true; + _lastZone = _clientState.TerritoryType; + Mediator.Publish(new DalamudLoginMessage()); + } + else if (localPlayer == null && IsLoggedIn) + { + _logger.LogDebug("Logged out"); + IsLoggedIn = false; + Mediator.Publish(new DalamudLogoutMessage()); + } + + if (IsInCombatOrPerforming) + Mediator.Publish(new FrameworkUpdateMessage()); + + Mediator.Publish(new DelayedFrameworkUpdateMessage()); + + _delayedFrameworkUpdateCheck = DateTime.UtcNow; + }); + } +} diff --git a/MareSynchronos/Services/Events/Event.cs b/MareSynchronos/Services/Events/Event.cs new file mode 100644 index 0000000..3f5bead --- /dev/null +++ b/MareSynchronos/Services/Events/Event.cs @@ -0,0 +1,45 @@ +using MareSynchronos.API.Data; + +namespace MareSynchronos.Services.Events; + +public record Event +{ + public DateTime EventTime { get; } + public string UID { get; } + public string Character { get; } + public string EventSource { get; } + public EventSeverity EventSeverity { get; } + public string Message { get; } + + public Event(string? Character, UserData UserData, string EventSource, EventSeverity EventSeverity, string Message) + { + EventTime = DateTime.Now; + this.UID = UserData.AliasOrUID; + this.Character = Character ?? string.Empty; + this.EventSource = EventSource; + this.EventSeverity = EventSeverity; + this.Message = Message; + } + + public Event(UserData UserData, string EventSource, EventSeverity EventSeverity, string Message) : this(null, UserData, EventSource, EventSeverity, Message) + { + } + + public Event(string EventSource, EventSeverity EventSeverity, string Message) + : this(new UserData(string.Empty), EventSource, EventSeverity, Message) + { + } + + public override string ToString() + { + if (string.IsNullOrEmpty(UID)) + return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t{Message}"; + else + { + if (string.IsNullOrEmpty(Character)) + return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{UID}> {Message}"; + else + return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{UID}\\{Character}> {Message}"; + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/Events/EventAggregator.cs b/MareSynchronos/Services/Events/EventAggregator.cs new file mode 100644 index 0000000..482ef93 --- /dev/null +++ b/MareSynchronos/Services/Events/EventAggregator.cs @@ -0,0 +1,113 @@ +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services.Events; + +public class EventAggregator : MediatorSubscriberBase, IHostedService +{ + private readonly RollingList _events = new(500); + private readonly SemaphoreSlim _lock = new(1); + private readonly string _configDirectory; + private readonly ILogger _logger; + + public Lazy> EventList { get; private set; } + public bool NewEventsAvailable => !EventList.IsValueCreated; + public string EventLogFolder => Path.Combine(_configDirectory, "eventlog"); + private string CurrentLogName => $"{DateTime.Now:yyyy-MM-dd}-events.log"; + private DateTime _currentTime; + + public EventAggregator(MareConfigService configService, ILogger logger, MareMediator mareMediator) : base(logger, mareMediator) + { + Mediator.Subscribe(this, (msg) => + { + _lock.Wait(); + try + { + Logger.LogTrace("Received Event: {evt}", msg.Event.ToString()); + _events.Add(msg.Event); + if (configService.Current.LogEvents) + WriteToFile(msg.Event); + } + finally + { + _lock.Release(); + } + + RecreateLazy(); + }); + + EventList = CreateEventLazy(); + _configDirectory = configService.ConfigurationDirectory; + _logger = logger; + _currentTime = DateTime.Now - TimeSpan.FromDays(1); + } + + private void RecreateLazy() + { + if (!EventList.IsValueCreated) return; + + EventList = CreateEventLazy(); + } + + private Lazy> CreateEventLazy() + { + return new Lazy>(() => + { + _lock.Wait(); + try + { + return [.. _events]; + } + finally + { + _lock.Release(); + } + }); + } + + private void WriteToFile(Event receivedEvent) + { + if (DateTime.Now.Day != _currentTime.Day) + { + try + { + _currentTime = DateTime.Now; + var filesInDirectory = Directory.EnumerateFiles(EventLogFolder, "*.log"); + if (filesInDirectory.Skip(10).Any()) + { + File.Delete(filesInDirectory.OrderBy(f => new FileInfo(f).LastWriteTimeUtc).First()); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not delete last events"); + } + } + + var eventLogFile = Path.Combine(EventLogFolder, CurrentLogName); + try + { + if (!Directory.Exists(EventLogFolder)) Directory.CreateDirectory(EventLogFolder); + File.AppendAllLines(eventLogFile, [receivedEvent.ToString()]); + } + catch (Exception ex) + { + _logger.LogWarning(ex, $"Could not write to event file {eventLogFile}"); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + Logger.LogInformation("Starting EventAggregatorService"); + Logger.LogInformation("Started EventAggregatorService"); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/MareSynchronos/Services/Events/EventSeverity.cs b/MareSynchronos/Services/Events/EventSeverity.cs new file mode 100644 index 0000000..aafb0cf --- /dev/null +++ b/MareSynchronos/Services/Events/EventSeverity.cs @@ -0,0 +1,8 @@ +namespace MareSynchronos.Services.Events; + +public enum EventSeverity +{ + Informational = 0, + Warning = 1, + Error = 2 +} diff --git a/MareSynchronos/Services/GuiHookService.cs b/MareSynchronos/Services/GuiHookService.cs new file mode 100644 index 0000000..32ea6ad --- /dev/null +++ b/MareSynchronos/Services/GuiHookService.cs @@ -0,0 +1,144 @@ +using Dalamud.Game.Gui.NamePlate; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Plugin.Services; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.UI; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public class GuiHookService : DisposableMediatorSubscriberBase +{ + private readonly ILogger _logger; + private readonly DalamudUtilService _dalamudUtil; + private readonly MareConfigService _configService; + private readonly INamePlateGui _namePlateGui; + private readonly IGameConfig _gameConfig; + private readonly IPartyList _partyList; + private readonly PairManager _pairManager; + + private bool _isModified = false; + private bool _namePlateRoleColorsEnabled = false; + + public GuiHookService(ILogger logger, DalamudUtilService dalamudUtil, MareMediator mediator, MareConfigService configService, + INamePlateGui namePlateGui, IGameConfig gameConfig, IPartyList partyList, PairManager pairManager) + : base(logger, mediator) + { + _logger = logger; + _dalamudUtil = dalamudUtil; + _configService = configService; + _namePlateGui = namePlateGui; + _gameConfig = gameConfig; + _partyList = partyList; + _pairManager = pairManager; + + _namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate; + _namePlateGui.RequestRedraw(); + + Mediator.Subscribe(this, (_) => GameSettingsCheck()); + Mediator.Subscribe(this, (_) => RequestRedraw()); + Mediator.Subscribe(this, (_) => RequestRedraw()); + } + + public void RequestRedraw(bool force = false) + { + if (!_configService.Current.UseNameColors) + { + if (!_isModified && !force) + return; + _isModified = false; + } + + _ = Task.Run(async () => { + await _dalamudUtil.RunOnFrameworkThread(() => _namePlateGui.RequestRedraw()).ConfigureAwait(false); + }); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate; + + _ = Task.Run(async () => { + await _dalamudUtil.RunOnFrameworkThread(() => _namePlateGui.RequestRedraw()).ConfigureAwait(false); + }); + } + + private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList handlers) + { + if (!_configService.Current.UseNameColors) + return; + + var visibleUsers = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue); + var visibleUsersIds = visibleUsers.Select(u => (ulong)u.PlayerCharacterId).ToHashSet(); + + var visibleUsersDict = visibleUsers.ToDictionary(u => (ulong)u.PlayerCharacterId); + + var partyMembers = new nint[_partyList.Count]; + + for (int i = 0; i < _partyList.Count; ++i) + partyMembers[i] = _partyList[i]?.GameObject?.Address ?? nint.MaxValue; + + foreach (var handler in handlers) + { + if (handler != null && visibleUsersIds.Contains(handler.GameObjectId)) + { + if (_namePlateRoleColorsEnabled && partyMembers.Contains(handler.GameObject?.Address ?? nint.MaxValue)) + continue; + var pair = visibleUsersDict[handler.GameObjectId]; + var colors = !pair.IsApplicationBlocked ? _configService.Current.NameColors : _configService.Current.BlockedNameColors; + handler.NameParts.TextWrap = ( + BuildColorStartSeString(colors), + BuildColorEndSeString(colors) + ); + _isModified = true; + } + } + } + + private void GameSettingsCheck() + { + if (!_gameConfig.TryGet(Dalamud.Game.Config.UiConfigOption.NamePlateSetRoleColor, out bool namePlateRoleColorsEnabled)) + return; + + if (_namePlateRoleColorsEnabled != namePlateRoleColorsEnabled) + { + _namePlateRoleColorsEnabled = namePlateRoleColorsEnabled; + RequestRedraw(force: true); + } + } + + #region Colored SeString + private const byte _colorTypeForeground = 0x13; + private const byte _colorTypeGlow = 0x14; + + private static SeString BuildColorStartSeString(DtrEntry.Colors colors) + { + var ssb = new SeStringBuilder(); + if (colors.Foreground != default) + ssb.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground)); + if (colors.Glow != default) + ssb.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow)); + return ssb.Build(); + } + + private static SeString BuildColorEndSeString(DtrEntry.Colors colors) + { + var ssb = new SeStringBuilder(); + if (colors.Glow != default) + ssb.Add(BuildColorEndPayload(_colorTypeGlow)); + if (colors.Foreground != default) + ssb.Add(BuildColorEndPayload(_colorTypeForeground)); + return ssb.Build(); + } + + private static RawPayload BuildColorStartPayload(byte colorType, uint color) + => new(unchecked([0x02, colorType, 0x05, 0xF6, byte.Max((byte)color, 0x01), byte.Max((byte)(color >> 8), 0x01), byte.Max((byte)(color >> 16), 0x01), 0x03])); + + private static RawPayload BuildColorEndPayload(byte colorType) + => new([0x02, colorType, 0x02, 0xEC, 0x03]); + #endregion +} diff --git a/MareSynchronos/Services/MareProfileData.cs b/MareSynchronos/Services/MareProfileData.cs new file mode 100644 index 0000000..af00ce7 --- /dev/null +++ b/MareSynchronos/Services/MareProfileData.cs @@ -0,0 +1,6 @@ +namespace MareSynchronos.Services; + +public record MareProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Description) +{ + public Lazy ImageData { get; } = new Lazy(Convert.FromBase64String(Base64ProfilePicture)); +} diff --git a/MareSynchronos/Services/MareProfileManager.cs b/MareSynchronos/Services/MareProfileManager.cs new file mode 100644 index 0000000..6bc3c50 --- /dev/null +++ b/MareSynchronos/Services/MareProfileManager.cs @@ -0,0 +1,78 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Comparer; +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace MareSynchronos.Services; + +public class MareProfileManager : MediatorSubscriberBase +{ + private const string _noDescription = "-- User has no description set --"; + private const string _nsfw = "Profile not displayed - NSFW"; + private readonly ApiController _apiController; + private readonly MareConfigService _mareConfigService; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly ConcurrentDictionary _mareProfiles = new(UserDataComparer.Instance); + + private readonly MareProfileData _defaultProfileData = new(IsFlagged: false, IsNSFW: false, string.Empty, _noDescription); + private readonly MareProfileData _loadingProfileData = new(IsFlagged: false, IsNSFW: false, string.Empty, "Loading Data from server..."); + private readonly MareProfileData _nsfwProfileData = new(IsFlagged: false, IsNSFW: false, string.Empty, _nsfw); + + public MareProfileManager(ILogger logger, MareConfigService mareConfigService, + MareMediator mediator, ApiController apiController, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator) + { + _mareConfigService = mareConfigService; + _apiController = apiController; + _serverConfigurationManager = serverConfigurationManager; + + Mediator.Subscribe(this, (msg) => + { + if (msg.UserData != null) + _mareProfiles.Remove(msg.UserData, out _); + else + _mareProfiles.Clear(); + }); + Mediator.Subscribe(this, (_) => _mareProfiles.Clear()); + } + + public MareProfileData GetMareProfile(UserData data) + { + if (!_mareProfiles.TryGetValue(data, out var profile)) + { + _ = Task.Run(() => GetMareProfileFromService(data)); + return (_loadingProfileData); + } + + return (profile); + } + + private async Task GetMareProfileFromService(UserData data) + { + try + { + _mareProfiles[data] = _loadingProfileData; + var profile = await _apiController.UserGetProfile(new API.Dto.User.UserDto(data)).ConfigureAwait(false); + MareProfileData profileData = new(profile.Disabled, profile.IsNSFW ?? false, + string.IsNullOrEmpty(profile.ProfilePictureBase64) ? string.Empty : profile.ProfilePictureBase64, + string.IsNullOrEmpty(profile.Description) ? _noDescription : profile.Description); + if (profileData.IsNSFW && !_mareConfigService.Current.ProfilesAllowNsfw && !string.Equals(_apiController.UID, data.UID, StringComparison.Ordinal)) + { + _mareProfiles[data] = _nsfwProfileData; + } + else + { + _mareProfiles[data] = profileData; + } + } + catch (Exception ex) + { + // if fails save DefaultProfileData to dict + Logger.LogWarning(ex, "Failed to get Profile from service for user {user}", data); + _mareProfiles[data] = _defaultProfileData; + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/DisposableMediatorSubscriberBase.cs b/MareSynchronos/Services/Mediator/DisposableMediatorSubscriberBase.cs new file mode 100644 index 0000000..d97cfaf --- /dev/null +++ b/MareSynchronos/Services/Mediator/DisposableMediatorSubscriberBase.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services.Mediator; + +public abstract class DisposableMediatorSubscriberBase : MediatorSubscriberBase, IDisposable +{ + protected DisposableMediatorSubscriberBase(ILogger logger, MareMediator mediator) : base(logger, mediator) + { + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + Logger.LogTrace("Disposing {type} ({this})", GetType().Name, this); + UnsubscribeAll(); + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/IMediatorSubscriber.cs b/MareSynchronos/Services/Mediator/IMediatorSubscriber.cs new file mode 100644 index 0000000..9f03cfa --- /dev/null +++ b/MareSynchronos/Services/Mediator/IMediatorSubscriber.cs @@ -0,0 +1,6 @@ +namespace MareSynchronos.Services.Mediator; + +public interface IMediatorSubscriber +{ + MareMediator Mediator { get; } +} \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/MareMediator.cs b/MareSynchronos/Services/Mediator/MareMediator.cs new file mode 100644 index 0000000..ebd0617 --- /dev/null +++ b/MareSynchronos/Services/Mediator/MareMediator.cs @@ -0,0 +1,222 @@ +using MareSynchronos.MareConfiguration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Reflection; +using System.Text; + +namespace MareSynchronos.Services.Mediator; + +public sealed class MareMediator : IHostedService +{ + private readonly Lock _addRemoveLock = new(); + private readonly ConcurrentDictionary _lastErrorTime = []; + private readonly ILogger _logger; + private readonly CancellationTokenSource _loopCts = new(); + private readonly ConcurrentQueue _messageQueue = new(); + private readonly PerformanceCollectorService _performanceCollector; + private readonly MareConfigService _mareConfigService; + private readonly ConcurrentDictionary<(Type, string?), HashSet> _subscriberDict = []; + private bool _processQueue = false; + private readonly ConcurrentDictionary<(Type, string?), MethodInfo?> _genericExecuteMethods = new(); + public MareMediator(ILogger logger, PerformanceCollectorService performanceCollector, MareConfigService mareConfigService) + { + _logger = logger; + _performanceCollector = performanceCollector; + _mareConfigService = mareConfigService; + } + + public void PrintSubscriberInfo() + { + foreach (var subscriber in _subscriberDict.SelectMany(c => c.Value.Select(v => v.Subscriber)) + .DistinctBy(p => p).OrderBy(p => p.GetType().FullName, StringComparer.Ordinal).ToList()) + { + _logger.LogInformation("Subscriber {type}: {sub}", subscriber.GetType().Name, subscriber.ToString()); + StringBuilder sb = new(); + sb.Append("=> "); + foreach (var item in _subscriberDict.Where(item => item.Value.Any(v => v.Subscriber == subscriber)).ToList()) + { + sb.Append(item.Key.Item1.Name); + if (item.Key.Item2 != null) + sb.Append($":{item.Key.Item2!}"); + sb.Append(", "); + } + + if (!string.Equals(sb.ToString(), "=> ", StringComparison.Ordinal)) + _logger.LogInformation("{sb}", sb.ToString()); + _logger.LogInformation("---"); + } + } + + public void Publish(T message) where T : MessageBase + { + if (message.KeepThreadContext) + { + ExecuteMessage(message); + } + else + { + _messageQueue.Enqueue(message); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting MareMediator"); + + _ = Task.Run(async () => + { + while (!_loopCts.Token.IsCancellationRequested) + { + while (!_processQueue) + { + await Task.Delay(100, _loopCts.Token).ConfigureAwait(false); + } + + await Task.Delay(100, _loopCts.Token).ConfigureAwait(false); + + while (_messageQueue.TryDequeue(out var message)) + { + ExecuteMessage(message); + } + } + }); + + _logger.LogInformation("Started MareMediator"); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _messageQueue.Clear(); + _loopCts.Cancel(); + return Task.CompletedTask; + } + + public void Subscribe(IMediatorSubscriber subscriber, Action action) where T : MessageBase + { + lock (_addRemoveLock) + { + _subscriberDict.TryAdd((typeof(T), null), []); + + if (!_subscriberDict[(typeof(T), null)].Add(new(subscriber, action))) + { + throw new InvalidOperationException("Already subscribed"); + } + + _logger.LogTrace("Subscriber added for message {message}: {sub}", typeof(T).Name, subscriber.GetType().Name); + } + } + + public void SubscribeKeyed(IMediatorSubscriber subscriber, string key, Action action) where T : MessageBase + { + lock (_addRemoveLock) + { + _subscriberDict.TryAdd((typeof(T), key), []); + + if (!_subscriberDict[(typeof(T), key)].Add(new(subscriber, action))) + { + throw new InvalidOperationException("Already subscribed"); + } + + _logger.LogTrace("Subscriber added for message {message}:{key}: {sub}", typeof(T).Name, key, subscriber.GetType().Name); + } + } + + public void Unsubscribe(IMediatorSubscriber subscriber) where T : MessageBase + { + lock (_addRemoveLock) + { + if (_subscriberDict.ContainsKey((typeof(T), null))) + { + _subscriberDict[(typeof(T), null)].RemoveWhere(p => p.Subscriber == subscriber); + } + } + } + + internal void UnsubscribeAll(IMediatorSubscriber subscriber) + { + lock (_addRemoveLock) + { + foreach (var kvp in _subscriberDict.Select(k => k.Key)) + { + int unSubbed = _subscriberDict[kvp]?.RemoveWhere(p => p.Subscriber == subscriber) ?? 0; + if (unSubbed > 0) + { + _logger.LogDebug("{sub} unsubscribed from {msg}", subscriber.GetType().Name, kvp.Item1.Name); + } + } + } + } + + private void ExecuteMessage(MessageBase message) + { + if (!_subscriberDict.TryGetValue((message.GetType(), message.SubscriberKey), out HashSet? subscribers) || subscribers == null || !subscribers.Any()) return; + + List subscribersCopy = []; + lock (_addRemoveLock) + { + subscribersCopy = subscribers?.Where(s => s.Subscriber != null).ToList() ?? []; + } + +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + var msgType = message.GetType(); + if (!_genericExecuteMethods.TryGetValue((msgType, message.SubscriberKey), out var methodInfo)) + { + _genericExecuteMethods[(msgType, message.SubscriberKey)] = methodInfo = GetType() + .GetMethod(nameof(ExecuteReflected), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)? + .MakeGenericMethod(msgType); + } + + methodInfo!.Invoke(this, [subscribersCopy, message]); +#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + } + + private void ExecuteReflected(List subscribers, T message) where T : MessageBase + { + foreach (SubscriberAction subscriber in subscribers) + { + try + { + if (_mareConfigService.Current.LogPerformance) + { + var isSameThread = message.KeepThreadContext ? "$" : string.Empty; + _performanceCollector.LogPerformance(this, $"{isSameThread}Execute>{message.GetType().Name}+{subscriber.Subscriber.GetType().Name}>{subscriber.Subscriber}", + () => ((Action)subscriber.Action).Invoke(message)); + } + else + { + ((Action)subscriber.Action).Invoke(message); + } + } + catch (Exception ex) + { + if (_lastErrorTime.TryGetValue(subscriber, out var lastErrorTime) && lastErrorTime.Add(TimeSpan.FromSeconds(10)) > DateTime.UtcNow) + continue; + + _logger.LogError(ex.InnerException ?? ex, "Error executing {type} for subscriber {subscriber}", + message.GetType().Name, subscriber.Subscriber.GetType().Name); + _lastErrorTime[subscriber] = DateTime.UtcNow; + } + } + } + + public void StartQueueProcessing() + { + _logger.LogInformation("Starting Message Queue Processing"); + _processQueue = true; + } + + private sealed class SubscriberAction + { + public SubscriberAction(IMediatorSubscriber subscriber, object action) + { + Subscriber = subscriber; + Action = action; + } + + public object Action { get; } + public IMediatorSubscriber Subscriber { get; } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/MediatorSubscriberBase.cs b/MareSynchronos/Services/Mediator/MediatorSubscriberBase.cs new file mode 100644 index 0000000..f45fee4 --- /dev/null +++ b/MareSynchronos/Services/Mediator/MediatorSubscriberBase.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services.Mediator; + +public abstract class MediatorSubscriberBase : IMediatorSubscriber +{ + protected MediatorSubscriberBase(ILogger logger, MareMediator mediator) + { + Logger = logger; + + Logger.LogTrace("Creating {type} ({this})", GetType().Name, this); + Mediator = mediator; + } + + public MareMediator Mediator { get; } + protected ILogger Logger { get; } + + protected void UnsubscribeAll() + { + Logger.LogTrace("Unsubscribing from all for {type} ({this})", GetType().Name, this); + Mediator.UnsubscribeAll(this); + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/MessageBase.cs b/MareSynchronos/Services/Mediator/MessageBase.cs new file mode 100644 index 0000000..40d9de2 --- /dev/null +++ b/MareSynchronos/Services/Mediator/MessageBase.cs @@ -0,0 +1,20 @@ +namespace MareSynchronos.Services.Mediator; + +#pragma warning disable MA0048 +public abstract record MessageBase +{ + public virtual bool KeepThreadContext => false; + public virtual string? SubscriberKey => null; +} + +public record SameThreadMessage : MessageBase +{ + public override bool KeepThreadContext => true; +} + +public record KeyedMessage(string MessageKey, bool SameThread = false) : MessageBase +{ + public override string? SubscriberKey => MessageKey; + public override bool KeepThreadContext => SameThread; +} +#pragma warning restore MA0048 \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs new file mode 100644 index 0000000..662b2e7 --- /dev/null +++ b/MareSynchronos/Services/Mediator/Messages.cs @@ -0,0 +1,113 @@ +using Dalamud.Game.ClientState.Objects.Types; +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Events; +using MareSynchronos.WebAPI.Files.Models; +using System.Numerics; + +namespace MareSynchronos.Services.Mediator; + +#pragma warning disable MA0048 // File name must match type name +#pragma warning disable S2094 +public record SwitchToIntroUiMessage : MessageBase; +public record SwitchToMainUiMessage : MessageBase; +public record OpenSettingsUiMessage : MessageBase; +public record DalamudLoginMessage : MessageBase; +public record DalamudLogoutMessage : MessageBase; +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 CutsceneStartMessage : MessageBase; +public record GposeStartMessage : SameThreadMessage; +public record GposeEndMessage : MessageBase; +public record CutsceneEndMessage : MessageBase; +public record CutsceneFrameworkUpdateMessage : SameThreadMessage; +public record ConnectedMessage(ConnectionDto Connection) : MessageBase; +public record DisconnectedMessage : SameThreadMessage; +public record PenumbraModSettingChangedMessage : MessageBase; +public record PenumbraInitializedMessage : MessageBase; +public record PenumbraDisposedMessage : MessageBase; +public record PenumbraRedrawMessage(IntPtr Address, int ObjTblIdx, bool WasRequested) : SameThreadMessage; +public record GlamourerChangedMessage(IntPtr Address) : MessageBase; +public record HeelsOffsetMessage : MessageBase; +public record PenumbraResourceLoadMessage(IntPtr GameObject, string GamePath, string FilePath) : SameThreadMessage; +public record CustomizePlusMessage(nint? Address) : MessageBase; +public record HonorificMessage(string NewHonorificTitle) : MessageBase; +public record PetNamesReadyMessage : MessageBase; +public record PetNamesMessage(string PetNicknamesData) : MessageBase; +public record MoodlesMessage(IntPtr Address) : MessageBase; +public record HonorificReadyMessage : MessageBase; +public record PlayerChangedMessage(CharacterData Data) : MessageBase; +public record CharacterChangedMessage(GameObjectHandler GameObjectHandler) : MessageBase; +public record TransientResourceChangedMessage(IntPtr Address) : MessageBase; +public record HaltScanMessage(string Source) : MessageBase; +public record ResumeScanMessage(string Source) : MessageBase; +public record NotificationMessage + (string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase; +public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase; +public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase; +public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage; +public record CharacterDataAnalyzedMessage : MessageBase; +public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase; +public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase; +public record HubReconnectingMessage(Exception? Exception) : SameThreadMessage; +public record HubReconnectedMessage(string? Arg) : SameThreadMessage; +public record HubClosedMessage(Exception? Exception) : SameThreadMessage; +public record DownloadReadyMessage(Guid RequestId) : MessageBase; +public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary DownloadStatus) : MessageBase; +public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase; +public record UiToggleMessage(Type UiType) : MessageBase; +public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase; +public record ClearProfileDataMessage(UserData? UserData = null) : MessageBase; +public record CyclePauseMessage(UserData UserData) : MessageBase; +public record PauseMessage(UserData UserData) : MessageBase; +public record ProfilePopoutToggle(Pair? Pair) : MessageBase; +public record CompactUiChange(Vector2 Size, Vector2 Position) : MessageBase; +public record ProfileOpenStandaloneMessage(Pair Pair) : MessageBase; +public record RemoveWindowMessage(WindowMediatorSubscriberBase Window) : MessageBase; +public record PlayerVisibilityMessage(string Ident, bool IsVisible, bool Invalidate = false) : KeyedMessage(Ident, SameThread: true); +public record PairHandlerVisibleMessage(PairHandler Player) : MessageBase; +public record OpenReportPopupMessage(Pair PairToReport) : MessageBase; +public record OpenBanUserPopupMessage(Pair PairToBan, GroupFullInfoDto GroupFullInfoDto) : MessageBase; +public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase; +public record OpenPermissionWindow(Pair Pair) : MessageBase; +public record OpenPairAnalysisWindow(Pair Pair) : MessageBase; +public record DownloadLimitChangedMessage() : SameThreadMessage; +public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase; +public record TargetPairMessage(Pair Pair) : MessageBase; +public record CombatOrPerformanceStartMessage : MessageBase; +public record CombatOrPerformanceEndMessage : MessageBase; +public record EventMessage(Event Event) : MessageBase; +public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBase; +public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage; +public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase; +public record GroupChatMsgMessage(GroupDto GroupInfo, SignedChatMessage ChatMsg) : MessageBase; +public record RecalculatePerformanceMessage(string? UID) : MessageBase; +public record NameplateRedrawMessage : MessageBase; +public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessage(UID); +public record UnholdPairApplicationMessage(string UID, string Source) : KeyedMessage(UID); +public record HoldPairDownloadsMessage(string UID, string Source) : KeyedMessage(UID); +public record UnholdPairDownloadsMessage(string UID, string Source) : KeyedMessage(UID); +public record PairDataAppliedMessage(string UID, CharacterData? CharacterData) : KeyedMessage(UID); +public record PairDataAnalyzedMessage(string UID) : KeyedMessage(UID); +public record GameObjectHandlerCreatedMessage(GameObjectHandler GameObjectHandler, bool OwnedObject) : MessageBase; +public record GameObjectHandlerDestroyedMessage(GameObjectHandler GameObjectHandler, bool OwnedObject) : MessageBase; +public record HaltCharaDataCreation(bool Resume = false) : SameThreadMessage; +public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase; +public record GposeLobbyUserJoin(UserData UserData) : MessageBase; +public record GPoseLobbyUserLeave(UserData UserData) : MessageBase; +public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadDto) : MessageBase; +public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase; +public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase; + +public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName); +#pragma warning restore S2094 +#pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/MareSynchronos/Services/Mediator/WindowMediatorSubscriberBase.cs b/MareSynchronos/Services/Mediator/WindowMediatorSubscriberBase.cs new file mode 100644 index 0000000..5e905fd --- /dev/null +++ b/MareSynchronos/Services/Mediator/WindowMediatorSubscriberBase.cs @@ -0,0 +1,54 @@ +using Dalamud.Interface.Windowing; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services.Mediator; + +public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber, IDisposable +{ + protected readonly ILogger _logger; + private readonly PerformanceCollectorService _performanceCollectorService; + + protected WindowMediatorSubscriberBase(ILogger logger, MareMediator mediator, string name, + PerformanceCollectorService performanceCollectorService) : base(name) + { + _logger = logger; + Mediator = mediator; + _performanceCollectorService = performanceCollectorService; + _logger.LogTrace("Creating {type}", GetType()); + + Mediator.Subscribe(this, (msg) => + { + if (msg.UiType == GetType()) + { + Toggle(); + } + }); + } + + public MareMediator Mediator { get; } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public override void Draw() + { + _performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal); + } + + protected abstract void DrawInternal(); + + public virtual Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + protected virtual void Dispose(bool disposing) + { + _logger.LogTrace("Disposing {type}", GetType()); + + Mediator.UnsubscribeAll(this); + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/NotificationService.cs b/MareSynchronos/Services/NotificationService.cs new file mode 100644 index 0000000..11ef63b --- /dev/null +++ b/MareSynchronos/Services/NotificationService.cs @@ -0,0 +1,141 @@ +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Plugin.Services; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType; + +namespace MareSynchronos.Services; + +public class NotificationService : DisposableMediatorSubscriberBase, IHostedService +{ + private readonly DalamudUtilService _dalamudUtilService; + private readonly INotificationManager _notificationManager; + private readonly IChatGui _chatGui; + private readonly MareConfigService _configurationService; + + public NotificationService(ILogger logger, MareMediator mediator, + DalamudUtilService dalamudUtilService, + INotificationManager notificationManager, + IChatGui chatGui, MareConfigService configurationService) : base(logger, mediator) + { + _dalamudUtilService = dalamudUtilService; + _notificationManager = notificationManager; + _chatGui = chatGui; + _configurationService = configurationService; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + Mediator.Subscribe(this, ShowNotification); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + private void PrintErrorChat(string? message) + { + SeStringBuilder se = new SeStringBuilder().AddText("[SnowcloakSync] Error: " + message); + _chatGui.PrintError(se.BuiltString); + } + + private void PrintInfoChat(string? message) + { + SeStringBuilder se = new SeStringBuilder().AddText("[SnowcloakSync] Info: ").AddItalics(message ?? string.Empty); + _chatGui.Print(se.BuiltString); + } + + private void PrintWarnChat(string? message) + { + SeStringBuilder se = new SeStringBuilder().AddText("[SnowcloakSync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff(); + _chatGui.Print(se.BuiltString); + } + + private void ShowChat(NotificationMessage msg) + { + switch (msg.Type) + { + case NotificationType.Info: + PrintInfoChat(msg.Message); + break; + + case NotificationType.Warning: + PrintWarnChat(msg.Message); + break; + + case NotificationType.Error: + PrintErrorChat(msg.Message); + break; + } + } + + private void ShowNotification(NotificationMessage msg) + { + Logger.LogInformation("{msg}", msg.ToString()); + + if (!_dalamudUtilService.IsLoggedIn) return; + + switch (msg.Type) + { + case NotificationType.Info: + ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification); + break; + + case NotificationType.Warning: + ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification); + break; + + case NotificationType.Error: + ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification); + break; + } + } + + private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location) + { + switch (location) + { + case NotificationLocation.Toast: + ShowToast(msg); + break; + + case NotificationLocation.Chat: + ShowChat(msg); + break; + + case NotificationLocation.Both: + ShowToast(msg); + ShowChat(msg); + break; + + case NotificationLocation.Nowhere: + break; + } + } + + private void ShowToast(NotificationMessage msg) + { + Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch + { + NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error, + NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning, + NotificationType.Info => Dalamud.Interface.ImGuiNotification.NotificationType.Info, + _ => Dalamud.Interface.ImGuiNotification.NotificationType.Info + }; + + _notificationManager.AddNotification(new Notification() + { + Content = msg.Message ?? string.Empty, + Title = msg.Title, + Type = dalamudType, + Minimized = false, + InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3) + }); + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/PairAnalyzer.cs b/MareSynchronos/Services/PairAnalyzer.cs new file mode 100644 index 0000000..f8f8267 --- /dev/null +++ b/MareSynchronos/Services/PairAnalyzer.cs @@ -0,0 +1,214 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.FileCache; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.UI; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public sealed class PairAnalyzer : DisposableMediatorSubscriberBase +{ + private readonly FileCacheManager _fileCacheManager; + private readonly XivDataAnalyzer _xivDataAnalyzer; + private CancellationTokenSource? _analysisCts; + private CancellationTokenSource _baseAnalysisCts = new(); + private string _lastDataHash = string.Empty; + + public PairAnalyzer(ILogger logger, Pair pair, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer) + : base(logger, mediator) + { + Pair = pair; +#if DEBUG + Mediator.SubscribeKeyed(this, pair.UserData.UID, (msg) => + { + _baseAnalysisCts = _baseAnalysisCts.CancelRecreate(); + var token = _baseAnalysisCts.Token; + if (msg.CharacterData != null) + { + _ = BaseAnalysis(msg.CharacterData, token); + } + else + { + LastAnalysis.Clear(); + _lastDataHash = string.Empty; + } + }); +#endif + _fileCacheManager = fileCacheManager; + _xivDataAnalyzer = modelAnalyzer; + +#if DEBUG + var lastReceivedData = pair.LastReceivedCharacterData; + if (lastReceivedData != null) + _ = BaseAnalysis(lastReceivedData, _baseAnalysisCts.Token); +#endif + } + + public Pair Pair { get; init; } + public int CurrentFile { get; internal set; } + public bool IsAnalysisRunning => _analysisCts != null; + public int TotalFiles { get; internal set; } + internal Dictionary> LastAnalysis { get; } = []; + internal string LastPlayerName { get; set; } = string.Empty; + + 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(); + TotalFiles = remaining.Count; + CurrentFile = 1; + Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count); + + Mediator.Publish(new HaltScanMessage(nameof(PairAnalyzer))); + try + { + foreach (var file in remaining) + { + Logger.LogDebug("Computing file {file}", file.FilePaths[0]); + await file.ComputeSizes(_fileCacheManager, cancelToken, ignoreCacheEntries: false).ConfigureAwait(false); + CurrentFile++; + } + + _fileCacheManager.WriteOutFullCsv(); + + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to analyze files"); + } + finally + { + Mediator.Publish(new ResumeScanMessage(nameof(PairAnalyzer))); + } + } + + LastPlayerName = Pair.PlayerName ?? string.Empty; + Mediator.Publish(new PairDataAnalyzedMessage(Pair.UserData.UID)); + + _analysisCts.CancelDispose(); + _analysisCts = null; + + if (print) PrintAnalysis(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (!disposing) return; + + _analysisCts?.CancelDispose(); + _baseAnalysisCts.CancelDispose(); + } + + 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); + foreach (var fileEntry in obj.Value) + { + token.ThrowIfCancellationRequested(); + + var fileCacheEntries = _fileCacheManager.GetAllFileCachesByHash(fileEntry.Hash, ignoreCacheEntries: false, validate: false).ToList(); + if (fileCacheEntries.Count == 0) continue; + + var filePath = fileCacheEntries[^1].ResolvedFilepath; + FileInfo fi = new(filePath); + string ext = "unk?"; + try + { + ext = fi.Extension[1..]; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Could not identify extension for {path}", filePath); + } + + var tris = await Task.Run(() => _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash)).ConfigureAwait(false); + + foreach (var entry in fileCacheEntries) + { + data[fileEntry.Hash] = new CharacterAnalyzer.FileDataEntry(fileEntry.Hash, ext, + [.. fileEntry.GamePaths], + fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal).ToList(), + entry.Size > 0 ? entry.Size.Value : 0, + entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0, + tris); + } + } + + LastAnalysis[obj.Key] = data; + } + + Mediator.Publish(new PairDataAnalyzedMessage(Pair.UserData.UID)); + + _lastDataHash = charaData.DataHash.Value; + } + + private void PrintAnalysis() + { + if (LastAnalysis.Count == 0) return; + foreach (var kvp in LastAnalysis) + { + int fileCounter = 1; + int totalFiles = kvp.Value.Count; + Logger.LogInformation("=== Analysis for {uid}:{obj} ===", Pair.UserData.UID, 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); + foreach (var path in entry.Value.GamePaths) + { + Logger.LogInformation(" Game Path: {path}", path); + } + if (entry.Value.FilePaths.Count > 1) Logger.LogInformation(" Multiple fitting files detected for {key}", entry.Key); + foreach (var filePath in entry.Value.FilePaths) + { + Logger.LogInformation(" File Path: {path}", filePath); + } + Logger.LogInformation(" Size: {size}, Compressed: {compressed}", UiSharedService.ByteToString(entry.Value.OriginalSize), + UiSharedService.ByteToString(entry.Value.CompressedSize)); + } + } + foreach (var kvp in LastAnalysis) + { + Logger.LogInformation("=== Detailed summary by file type for {obj} ===", kvp.Key); + foreach (var entry in kvp.Value.Select(v => v.Value).GroupBy(v => v.FileType, StringComparer.Ordinal)) + { + Logger.LogInformation("{ext} files: {count}, size extracted: {size}, size compressed: {sizeComp}", entry.Key, entry.Count(), + UiSharedService.ByteToString(entry.Sum(v => v.OriginalSize)), UiSharedService.ByteToString(entry.Sum(v => v.CompressedSize))); + } + Logger.LogInformation("=== Total summary for {obj} ===", kvp.Key); + 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), + UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.OriginalSize))), + UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize)))); + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/PerformanceCollectorService.cs b/MareSynchronos/Services/PerformanceCollectorService.cs new file mode 100644 index 0000000..fad205c --- /dev/null +++ b/MareSynchronos/Services/PerformanceCollectorService.cs @@ -0,0 +1,199 @@ +using MareSynchronos.MareConfiguration; +using MareSynchronos.Utils; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Globalization; +using System.Text; + +namespace MareSynchronos.Services; + +public sealed class PerformanceCollectorService : IHostedService +{ + private const string _counterSplit = "=>"; + private readonly ILogger _logger; + private readonly MareConfigService _mareConfigService; + public ConcurrentDictionary> PerformanceCounters { get; } = new(StringComparer.Ordinal); + private readonly CancellationTokenSource _periodicLogPruneTaskCts = new(); + + public PerformanceCollectorService(ILogger logger, MareConfigService mareConfigService) + { + _logger = logger; + _mareConfigService = mareConfigService; + } + + public T LogPerformance(object sender, MareInterpolatedStringHandler counterName, Func func, int maxEntries = 10000) + { + if (!_mareConfigService.Current.LogPerformance) return func.Invoke(); + + string cn = sender.GetType().Name + _counterSplit + counterName.BuildMessage(); + + if (!PerformanceCounters.TryGetValue(cn, out var list)) + { + list = PerformanceCounters[cn] = new(maxEntries); + } + + var dt = DateTime.UtcNow.Ticks; + try + { + return func.Invoke(); + } + finally + { + var elapsed = DateTime.UtcNow.Ticks - dt; +#if DEBUG + if (TimeSpan.FromTicks(elapsed) > TimeSpan.FromMilliseconds(10)) + _logger.LogWarning(">10ms spike on {counterName}: {time}", cn, TimeSpan.FromTicks(elapsed)); +#endif + list.Add((TimeOnly.FromDateTime(DateTime.Now), elapsed)); + } + } + + public void LogPerformance(object sender, MareInterpolatedStringHandler counterName, Action act, int maxEntries = 10000) + { + if (!_mareConfigService.Current.LogPerformance) { act.Invoke(); return; } + + var cn = sender.GetType().Name + _counterSplit + counterName.BuildMessage(); + + if (!PerformanceCounters.TryGetValue(cn, out var list)) + { + list = PerformanceCounters[cn] = new(maxEntries); + } + + var dt = DateTime.UtcNow.Ticks; + try + { + act.Invoke(); + } + finally + { + var elapsed = DateTime.UtcNow.Ticks - dt; +#if DEBUG + 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)); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting PerformanceCollectorService"); + _ = Task.Run(PeriodicLogPrune, _periodicLogPruneTaskCts.Token); + _logger.LogInformation("Started PerformanceCollectorService"); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _periodicLogPruneTaskCts.Cancel(); + _periodicLogPruneTaskCts.Dispose(); + return Task.CompletedTask; + } + + internal void PrintPerformanceStats(int limitBySeconds = 0) + { + if (!_mareConfigService.Current.LogPerformance) + { + _logger.LogWarning("Performance counters are disabled"); + } + + StringBuilder sb = new(); + if (limitBySeconds > 0) + { + sb.AppendLine($"Performance Metrics over the past {limitBySeconds} seconds of each counter"); + } + else + { + sb.AppendLine("Performance metrics over total lifetime of each counter"); + } + var data = PerformanceCounters.ToList(); + var longestCounterName = data.OrderByDescending(d => d.Key.Length).First().Key.Length + 2; + sb.Append("-Last".PadRight(15, '-')); + sb.Append('|'); + sb.Append("-Max".PadRight(15, '-')); + sb.Append('|'); + sb.Append("-Average".PadRight(15, '-')); + sb.Append('|'); + sb.Append("-Last Update".PadRight(15, '-')); + sb.Append('|'); + sb.Append("-Entries".PadRight(10, '-')); + 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]; + foreach (var entry in orderedData) + { + var newCaller = entry.Key.Split(_counterSplit, StringSplitOptions.RemoveEmptyEntries)[0]; + if (!string.Equals(previousCaller, newCaller, StringComparison.Ordinal)) + { + DrawSeparator(sb, longestCounterName); + } + + var pastEntries = limitBySeconds > 0 ? entry.Value.Where(e => e.Item1.AddMinutes(limitBySeconds / 60.0d) >= TimeOnly.FromDateTime(DateTime.Now)).ToList() : [.. entry.Value]; + + if (pastEntries.Any()) + { + sb.Append((" " + TimeSpan.FromTicks(pastEntries.LastOrDefault() == default ? 0 : pastEntries.Last().Item2).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15)); + sb.Append('|'); + sb.Append((" " + TimeSpan.FromTicks(pastEntries.Max(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15)); + sb.Append('|'); + sb.Append((" " + TimeSpan.FromTicks((long)pastEntries.Average(m => m.Item2)).TotalMilliseconds.ToString("0.00000", CultureInfo.InvariantCulture)).PadRight(15)); + sb.Append('|'); + sb.Append((" " + (pastEntries.LastOrDefault() == default ? "-" : pastEntries.Last().Item1.ToString("HH:mm:ss.ffff", CultureInfo.InvariantCulture))).PadRight(15, ' ')); + sb.Append('|'); + sb.Append((" " + pastEntries.Count).PadRight(10)); + sb.Append('|'); + sb.Append(' ').Append(entry.Key); + sb.AppendLine(); + } + + previousCaller = newCaller; + } + + DrawSeparator(sb, longestCounterName); + + _logger.LogInformation("{perf}", sb.ToString()); + } + + private static void DrawSeparator(StringBuilder sb, int longestCounterName) + { + sb.Append("".PadRight(15, '-')); + sb.Append('+'); + sb.Append("".PadRight(15, '-')); + sb.Append('+'); + sb.Append("".PadRight(15, '-')); + sb.Append('+'); + sb.Append("".PadRight(15, '-')); + sb.Append('+'); + sb.Append("".PadRight(10, '-')); + sb.Append('+'); + sb.Append("".PadRight(longestCounterName, '-')); + sb.AppendLine(); + } + + private async Task PeriodicLogPrune() + { + while (!_periodicLogPruneTaskCts.Token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromMinutes(10), _periodicLogPruneTaskCts.Token).ConfigureAwait(false); + + foreach (var entries in PerformanceCounters.ToList()) + { + try + { + var last = entries.Value.ToList().Last(); + if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !PerformanceCounters.TryRemove(entries.Key, out _)) + { + _logger.LogDebug("Could not remove performance counter {counter}", entries.Key); + } + } + catch (Exception e) + { + _logger.LogWarning(e, "Error removing performance counter {counter}", entries.Key); + } + } + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/PlayerPerformanceService.cs b/MareSynchronos/Services/PlayerPerformanceService.cs new file mode 100644 index 0000000..fed2792 --- /dev/null +++ b/MareSynchronos/Services/PlayerPerformanceService.cs @@ -0,0 +1,330 @@ +using MareSynchronos.API.Data; +using MareSynchronos.FileCache; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services.Events; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI; +using MareSynchronos.WebAPI.Files.Models; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public class PlayerPerformanceService : DisposableMediatorSubscriberBase +{ + // Limits that will still be enforced when no limits are enabled + public const int MaxVRAMUsageThreshold = 2000; // 2GB + public const int MaxTriUsageThreshold = 2000000; // 2 million triangles + + private readonly FileCacheManager _fileCacheManager; + private readonly XivDataAnalyzer _xivDataAnalyzer; + private readonly ILogger _logger; + private readonly MareMediator _mediator; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; + private readonly Dictionary _warnedForPlayers = new(StringComparer.Ordinal); + + public PlayerPerformanceService(ILogger logger, MareMediator mediator, + ServerConfigurationManager serverConfigurationManager, + PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager, + XivDataAnalyzer xivDataAnalyzer) + : base(logger, mediator) + { + _logger = logger; + _mediator = mediator; + _serverConfigurationManager = serverConfigurationManager; + _playerPerformanceConfigService = playerPerformanceConfigService; + _fileCacheManager = fileCacheManager; + _xivDataAnalyzer = xivDataAnalyzer; + } + + public async Task CheckBothThresholds(PairHandler pairHandler, CharacterData charaData) + { + bool notPausedAfterVram = ComputeAndAutoPauseOnVRAMUsageThresholds(pairHandler, charaData, []); + if (!notPausedAfterVram) return false; + bool notPausedAfterTris = await CheckTriangleUsageThresholds(pairHandler, charaData).ConfigureAwait(false); + if (!notPausedAfterTris) return false; + + return true; + } + + public async Task CheckTriangleUsageThresholds(PairHandler pairHandler, CharacterData charaData) + { + var config = _playerPerformanceConfigService.Current; + var pair = pairHandler.Pair; + + long triUsage = 0; + + var moddedModelHashes = charaData.FileReplacements.SelectMany(k => k.Value) + .Where(p => string.IsNullOrEmpty(p.FileSwapPath) && p.GamePaths.Any(g => g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase))) + .Select(p => p.Hash) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var hash in moddedModelHashes) + { + triUsage += await Task.Run(() => _xivDataAnalyzer.GetTrianglesByHash(hash)).ConfigureAwait(false); + } + + pair.LastAppliedDataTris = triUsage; + + _logger.LogDebug("Calculated Triangle usage for {p}", pairHandler); + + long triUsageThreshold = config.TrisAutoPauseThresholdThousands * 1000; + bool isDirect = pair.UserPair != null; + bool autoPause = config.AutoPausePlayersExceedingThresholds; + bool notify = isDirect ? config.NotifyAutoPauseDirectPairs : config.NotifyAutoPauseGroupPairs; + + if (autoPause && isDirect && config.IgnoreDirectPairs) + autoPause = false; + + if (!autoPause || _serverConfigurationManager.IsUidWhitelisted(pair.UserData.UID)) + triUsageThreshold = MaxTriUsageThreshold; + + if (triUsage > triUsageThreshold) + { + if (notify && !pair.IsApplicationBlocked) + { + _mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically blocked", + $"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured triangle auto block threshold (" + + $"{triUsage}/{triUsageThreshold} triangles)" + + $" and has been automatically blocked.", + MareConfiguration.Models.NotificationType.Warning)); + } + + _mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning, + $"Exceeds triangle threshold: ({triUsage}/{triUsageThreshold} triangles)"))); + + return false; + } + + return true; + } + + public bool ComputeAndAutoPauseOnVRAMUsageThresholds(PairHandler pairHandler, CharacterData charaData, List toDownloadFiles, bool affect = false) + { + var config = _playerPerformanceConfigService.Current; + var pair = pairHandler.Pair; + + long vramUsage = 0; + + var moddedTextureHashes = charaData.FileReplacements.SelectMany(k => k.Value) + .Where(p => string.IsNullOrEmpty(p.FileSwapPath) && p.GamePaths.Any(g => g.EndsWith(".tex", StringComparison.OrdinalIgnoreCase))) + .Select(p => p.Hash) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var hash in moddedTextureHashes) + { + long fileSize = 0; + + var download = toDownloadFiles.Find(f => string.Equals(hash, f.Hash, StringComparison.OrdinalIgnoreCase)); + if (download != null) + { + fileSize = download.TotalRaw; + } + else + { + var fileEntry = _fileCacheManager.GetFileCacheByHash(hash, preferSubst: true); + if (fileEntry == null) continue; + + if (fileEntry.Size == null) + { + fileEntry.Size = new FileInfo(fileEntry.ResolvedFilepath).Length; + _fileCacheManager.UpdateHashedFile(fileEntry, computeProperties: true); + } + + fileSize = fileEntry.Size.Value; + } + + vramUsage += fileSize; + } + + pair.LastAppliedApproximateVRAMBytes = vramUsage; + + _logger.LogDebug("Calculated VRAM usage for {p}", pairHandler); + + long vramUsageThreshold = config.VRAMSizeAutoPauseThresholdMiB; + bool isDirect = pair.UserPair != null; + bool autoPause = config.AutoPausePlayersExceedingThresholds; + bool notify = isDirect ? config.NotifyAutoPauseDirectPairs : config.NotifyAutoPauseGroupPairs; + + if (autoPause && isDirect && config.IgnoreDirectPairs) + autoPause = false; + + if (!autoPause || _serverConfigurationManager.IsUidWhitelisted(pair.UserData.UID)) + vramUsageThreshold = MaxVRAMUsageThreshold; + + if (vramUsage > vramUsageThreshold * 1024 * 1024) + { + if (!affect) + return false; + + if (notify && !pair.IsApplicationBlocked) + { + _mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically blocked", + $"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured VRAM auto block threshold (" + + $"{UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{vramUsageThreshold}MiB)" + + $" and has been automatically blocked.", + MareConfiguration.Models.NotificationType.Warning)); + } + + _mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning, + $"Exceeds VRAM threshold: ({UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{vramUsageThreshold} MiB)"))); + + return false; + } + + return true; + } + + public async Task ShrinkTextures(PairHandler pairHandler, CharacterData charaData, CancellationToken token) + { + var config = _playerPerformanceConfigService.Current; + + if (config.TextureShrinkMode == MareConfiguration.Models.TextureShrinkMode.Never) + return false; + + // XXX: Temporary + if (config.TextureShrinkMode == MareConfiguration.Models.TextureShrinkMode.Default) + return false; + + var moddedTextureHashes = charaData.FileReplacements.SelectMany(k => k.Value) + .Where(p => string.IsNullOrEmpty(p.FileSwapPath) && p.GamePaths.Any(g => g.EndsWith(".tex", StringComparison.OrdinalIgnoreCase))) + .Select(p => p.Hash) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + bool shrunken = false; + + await Parallel.ForEachAsync(moddedTextureHashes, + token, + async (hash, token) => { + var fileEntry = _fileCacheManager.GetFileCacheByHash(hash, preferSubst: true); + if (fileEntry == null) return; + if (fileEntry.IsSubstEntry) return; + + var texFormat = _xivDataAnalyzer.GetTexFormatByHash(hash); + var filePath = fileEntry.ResolvedFilepath; + var tmpFilePath = _fileCacheManager.GetSubstFilePath(Guid.NewGuid().ToString(), "tmp"); + var newFilePath = _fileCacheManager.GetSubstFilePath(hash, "tex"); + var mipLevel = 0; + uint width = texFormat.Width; + uint height = texFormat.Height; + long offsetDelta = 0; + + uint bitsPerPixel = texFormat.Format switch + { + 0x1130 => 8, // L8 + 0x1131 => 8, // A8 + 0x1440 => 16, // A4R4G4B4 + 0x1441 => 16, // A1R5G5B5 + 0x1450 => 32, // A8R8G8B8 + 0x1451 => 32, // X8R8G8B8 + 0x2150 => 32, // R32F + 0x2250 => 32, // G16R16F + 0x2260 => 64, // R32G32F + 0x2460 => 64, // A16B16G16R16F + 0x2470 => 128, // A32B32G32R32F + 0x3420 => 4, // DXT1 + 0x3430 => 8, // DXT3 + 0x3431 => 8, // DXT5 + 0x4140 => 16, // D16 + 0x4250 => 32, // D24S8 + 0x6120 => 4, // BC4 + 0x6230 => 8, // BC5 + 0x6432 => 8, // BC7 + _ => 0 + }; + + uint maxSize = (bitsPerPixel <= 8) ? (2048U * 2048U) : (1024U * 1024U); + + while (width * height > maxSize && mipLevel < texFormat.MipCount - 1) + { + offsetDelta += width * height * bitsPerPixel / 8; + mipLevel++; + width /= 2; + height /= 2; + } + + if (offsetDelta == 0) + return; + + _logger.LogDebug("Shrinking {hash} from from {a}x{b} to {c}x{d}", + hash, texFormat.Width, texFormat.Height, width, height); + + try + { + var inFile = new FileStream(filePath, FileMode.Open, FileAccess.Read); + using var reader = new BinaryReader(inFile); + + var header = reader.ReadBytes(80); + reader.BaseStream.Position = 14; + byte mipByte = reader.ReadByte(); + byte mipCount = (byte)(mipByte & 0x7F); + + var outFile = new FileStream(tmpFilePath, FileMode.Create, FileAccess.Write, FileShare.None); + using var writer = new BinaryWriter(outFile); + writer.Write(header); + + // Update width/height + writer.BaseStream.Position = 8; + writer.Write((ushort)width); + writer.Write((ushort)height); + + // Update the mip count + writer.BaseStream.Position = 14; + writer.Write((ushort)((mipByte & 0x80) | (mipCount - mipLevel))); + + // Reset all of the LoD mips + writer.BaseStream.Position = 16; + for (int i = 0; i < 3; ++i) + writer.Write((uint)0); + + // Reset all of the mip offsets + // (This data is garbage in a lot of modded textures, so its hard to fix it up correctly) + writer.BaseStream.Position = 28; + for (int i = 0; i < 13; ++i) + writer.Write((uint)80); + + // Write the texture data shifted + outFile.Position = 80; + inFile.Position = 80 + offsetDelta; + + await inFile.CopyToAsync(outFile, 81920, token).ConfigureAwait(false); + + reader.Dispose(); + writer.Dispose(); + + File.Move(tmpFilePath, newFilePath); + var substEntry = _fileCacheManager.CreateSubstEntry(newFilePath); + if (substEntry != null) + substEntry.CompressedSize = fileEntry.CompressedSize; + shrunken = true; + + // Make sure its a cache file before trying to delete it !! + bool shouldDelete = fileEntry.IsCacheEntry && File.Exists(filePath); + + if (_playerPerformanceConfigService.Current.TextureShrinkDeleteOriginal && shouldDelete) + { + try + { + _logger.LogDebug("Deleting original texture: {filePath}", filePath); + File.Delete(filePath); + } + catch { } + } + } + catch (Exception e) + { + _logger.LogWarning(e, "Failed to shrink texture {hash}", hash); + if (File.Exists(tmpFilePath)) + File.Delete(tmpFilePath); + } + } + ).ConfigureAwait(false); + + return shrunken; + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/PluginWarningNotificationService.cs b/MareSynchronos/Services/PluginWarningNotificationService.cs new file mode 100644 index 0000000..337f93b --- /dev/null +++ b/MareSynchronos/Services/PluginWarningNotificationService.cs @@ -0,0 +1,76 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Comparer; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services.Mediator; +using System.Collections.Concurrent; + +namespace MareSynchronos.PlayerData.Pairs; + +public class PluginWarningNotificationService +{ + private readonly ConcurrentDictionary _cachedOptionalPluginWarnings = new(UserDataComparer.Instance); + private readonly IpcManager _ipcManager; + private readonly MareConfigService _mareConfigService; + private readonly MareMediator _mediator; + + public PluginWarningNotificationService(MareConfigService mareConfigService, IpcManager ipcManager, MareMediator mediator) + { + _mareConfigService = mareConfigService; + _ipcManager = ipcManager; + _mediator = mediator; + } + + public void NotifyForMissingPlugins(UserData user, string playerName, HashSet changes) + { + if (!_cachedOptionalPluginWarnings.TryGetValue(user, out var warning)) + { + _cachedOptionalPluginWarnings[user] = warning = new() + { + ShownCustomizePlusWarning = _mareConfigService.Current.DisableOptionalPluginWarnings, + ShownHeelsWarning = _mareConfigService.Current.DisableOptionalPluginWarnings, + ShownHonorificWarning = _mareConfigService.Current.DisableOptionalPluginWarnings, + ShowPetNicknamesWarning = _mareConfigService.Current.DisableOptionalPluginWarnings, + ShownMoodlesWarning = _mareConfigService.Current.DisableOptionalPluginWarnings + }; + } + + List missingPluginsForData = []; + if (changes.Contains(PlayerChanges.Heels) && !warning.ShownHeelsWarning && !_ipcManager.Heels.APIAvailable) + { + missingPluginsForData.Add("SimpleHeels"); + warning.ShownHeelsWarning = true; + } + if (changes.Contains(PlayerChanges.Customize) && !warning.ShownCustomizePlusWarning && !_ipcManager.CustomizePlus.APIAvailable) + { + missingPluginsForData.Add("Customize+"); + warning.ShownCustomizePlusWarning = true; + } + + if (changes.Contains(PlayerChanges.Honorific) && !warning.ShownHonorificWarning && !_ipcManager.Honorific.APIAvailable) + { + missingPluginsForData.Add("Honorific"); + warning.ShownHonorificWarning = true; + } + + if (changes.Contains(PlayerChanges.PetNames) && !warning.ShowPetNicknamesWarning && !_ipcManager.PetNames.APIAvailable) + { + missingPluginsForData.Add("PetNicknames"); + warning.ShowPetNicknamesWarning = true; + } + + if (changes.Contains(PlayerChanges.Moodles) && !warning.ShownMoodlesWarning && !_ipcManager.Moodles.APIAvailable) + { + missingPluginsForData.Add("Moodles"); + warning.ShownMoodlesWarning = true; + } + + if (missingPluginsForData.Any()) + { + _mediator.Publish(new NotificationMessage("Missing plugins for " + playerName, + $"Received data for {playerName} that contained information for plugins you have not installed. Install {string.Join(", ", missingPluginsForData)} to experience their character fully.", + NotificationType.Warning, TimeSpan.FromSeconds(10))); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/PluginWatcherService.cs b/MareSynchronos/Services/PluginWatcherService.cs new file mode 100644 index 0000000..73d8630 --- /dev/null +++ b/MareSynchronos/Services/PluginWatcherService.cs @@ -0,0 +1,160 @@ +using Dalamud.Plugin; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using CapturedPluginState = (string InternalName, System.Version Version, bool IsLoaded); + +namespace MareSynchronos.Services; + +/* Parts of this code from ECommons DalamudReflector + +MIT License + +Copyright (c) 2023 NightmareXIV + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +public class PluginWatcherService : MediatorSubscriberBase, IHostedService +{ + private readonly IDalamudPluginInterface _pluginInterface; + + private CapturedPluginState[] _prevInstalledPluginState = []; + +#pragma warning disable + private static bool ExposedPluginsEqual(IEnumerable plugins, IEnumerable other) + { + if (plugins.Count() != other.Count()) return false; + var enumeratorOriginal = plugins.GetEnumerator(); + var enumeratorOther = other.GetEnumerator(); + while (true) + { + var move1 = enumeratorOriginal.MoveNext(); + var move2 = enumeratorOther.MoveNext(); + if (move1 != move2) return false; + if (move1 == false) return true; + if (enumeratorOriginal.Current.IsLoaded != enumeratorOther.Current.IsLoaded) return false; + if (enumeratorOriginal.Current.Version != enumeratorOther.Current.Version) return false; + if (enumeratorOriginal.Current.InternalName != enumeratorOther.Current.InternalName) return false; + } + } +#pragma warning restore + + public PluginWatcherService(ILogger logger, IDalamudPluginInterface pluginInterface, MareMediator mediator) : base(logger, mediator) + { + _pluginInterface = pluginInterface; + + Mediator.Subscribe(this, (_) => + { + try + { + Update(); + } + catch (Exception e) + { + Logger.LogError(e, "PluginWatcherService exception"); + } + }); + + // Continue scanning plugins during gpose as well + Mediator.Subscribe(this, (_) => + { + try + { + Update(); + } + catch (Exception e) + { + Logger.LogError(e, "PluginWatcherService exception"); + } + }); + + Update(publish: false); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Mediator.UnsubscribeAll(this); + return Task.CompletedTask; + } + + public static PluginChangeMessage? GetInitialPluginState(IDalamudPluginInterface pi, string internalName) + { + try + { + var plugin = pi.InstalledPlugins.Where(p => p.InternalName.Equals(internalName, StringComparison.Ordinal)) + .OrderBy(p => (!p.IsLoaded, p.Version)) + .FirstOrDefault(); + + if (plugin == null) + return null; + + return new PluginChangeMessage(plugin.InternalName, plugin.Version, plugin.IsLoaded); + } + catch + { + return null; + } + } + + private void Update(bool publish = true) + { + if (!ExposedPluginsEqual(_pluginInterface.InstalledPlugins, _prevInstalledPluginState)) + { + var state = _pluginInterface.InstalledPlugins.Select(x => new CapturedPluginState(x.InternalName, x.Version, x.IsLoaded)).ToArray(); + + // The same plugin can be installed multiple times -- InternalName is not unique + + var oldDict = _prevInstalledPluginState.Where(x => x.InternalName.Length > 0) + .GroupBy(x => x.InternalName, StringComparer.Ordinal) + .ToDictionary(x => x.Key, StringComparer.Ordinal); + + var newDict = state.Where(x => x.InternalName.Length > 0) + .GroupBy(x => x.InternalName, StringComparer.Ordinal) + .ToDictionary(x => x.Key, StringComparer.Ordinal); + + _prevInstalledPluginState = state; + + foreach (var internalName in newDict.Keys.Except(oldDict.Keys, StringComparer.Ordinal)) + { + var p = newDict[internalName].OrderBy(p => (!p.IsLoaded, p.Version)).First(); + if (publish) Mediator.Publish(new PluginChangeMessage(internalName, p.Version, p.IsLoaded)); + } + + foreach (var internalName in oldDict.Keys.Except(newDict.Keys, StringComparer.Ordinal)) + { + var p = oldDict[internalName].OrderBy(p => (!p.IsLoaded, p.Version)).First(); + if (publish) Mediator.Publish(new PluginChangeMessage(p.InternalName, p.Version, IsLoaded: false)); + } + + foreach (var changedGroup in newDict.Where(p => oldDict.TryGetValue(p.Key, out var old) && !old.SequenceEqual(p.Value))) + { + var internalName = changedGroup.Value.First().InternalName; + var p = newDict[internalName].OrderBy(p => (!p.IsLoaded, p.Version)).First(); + if (publish) Mediator.Publish(new PluginChangeMessage(p.InternalName, p.Version, p.IsLoaded)); + } + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs new file mode 100644 index 0000000..768808e --- /dev/null +++ b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs @@ -0,0 +1,547 @@ +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace MareSynchronos.Services.ServerConfiguration; + +public class ServerConfigurationManager +{ + private readonly ServerConfigService _configService; + private readonly DalamudUtilService _dalamudUtil; + private readonly ILogger _logger; + private readonly NotesConfigService _notesConfig; + private readonly ServerBlockConfigService _blockConfig; + private readonly ServerTagConfigService _serverTagConfig; + private readonly SyncshellConfigService _syncshellConfig; + + private HashSet? _cachedWhitelistedUIDs = null; + private HashSet? _cachedBlacklistedUIDs = null; + private string? _realApiUrl = null; + + public ServerConfigurationManager(ILogger logger, ServerConfigService configService, + ServerTagConfigService serverTagConfig, SyncshellConfigService syncshellConfig, NotesConfigService notesConfig, + ServerBlockConfigService blockConfig, DalamudUtilService dalamudUtil) + { + _logger = logger; + _configService = configService; + _serverTagConfig = serverTagConfig; + _syncshellConfig = syncshellConfig; + _notesConfig = notesConfig; + _blockConfig = blockConfig; + _dalamudUtil = dalamudUtil; + EnsureMainExists(); + } + + public string CurrentApiUrl => CurrentServer.ServerUri; + public string CurrentRealApiUrl + { + get + { + return _realApiUrl ?? CurrentApiUrl; + } + } + public ServerStorage CurrentServer => _configService.Current.ServerStorage[CurrentServerIndex]; + + public IReadOnlyList Whitelist => CurrentBlockStorage().Whitelist; + public IReadOnlyList Blacklist => CurrentBlockStorage().Blacklist; + + public int CurrentServerIndex + { + set + { + _configService.Current.CurrentServer = value; + _cachedWhitelistedUIDs = null; + _cachedBlacklistedUIDs = null; + _realApiUrl = null; + _configService.Save(); + } + get + { + if (_configService.Current.CurrentServer < 0) + { + _configService.Current.CurrentServer = 0; + _configService.Save(); + } + + return _configService.Current.CurrentServer; + } + } + + public string? GetSecretKey(out bool hasMulti, int serverIdx = -1) + { + ServerStorage? currentServer; + currentServer = serverIdx == -1 ? CurrentServer : GetServerByIndex(serverIdx); + if (currentServer == null) + { + currentServer = new(); + Save(); + } + hasMulti = false; + + var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(); + var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(); + if (!currentServer.Authentications.Any() && currentServer.SecretKeys.Any()) + { + currentServer.Authentications.Add(new Authentication() + { + CharacterName = charaName, + WorldId = worldId, + SecretKeyIdx = currentServer.SecretKeys.Last().Key, + }); + + Save(); + } + + var auth = currentServer.Authentications.FindAll(f => string.Equals(f.CharacterName, charaName, StringComparison.Ordinal) && f.WorldId == worldId); + if (auth.Count >= 2) + { + _logger.LogTrace("GetSecretKey accessed, returning null because multiple ({count}) identical characters.", auth.Count); + hasMulti = true; + return null; + } + + if (auth.Count == 0) + { + _logger.LogTrace("GetSecretKey accessed, returning null because no set up characters for {chara} on {world}", charaName, worldId); + return null; + } + + if (currentServer.SecretKeys.TryGetValue(auth.Single().SecretKeyIdx, out var secretKey)) + { + _logger.LogTrace("GetSecretKey accessed, returning {key} ({keyValue}) for {chara} on {world}", secretKey.FriendlyName, string.Join("", secretKey.Key.Take(10)), charaName, worldId); + return secretKey.Key; + } + + _logger.LogTrace("GetSecretKey accessed, returning null because no fitting key found for {chara} on {world} for idx {idx}.", charaName, worldId, auth.Single().SecretKeyIdx); + + return null; + } + + public string[] GetServerApiUrls() + { + return _configService.Current.ServerStorage.Select(v => v.ServerUri).ToArray(); + } + + public ServerStorage GetServerByIndex(int idx) + { + try + { + return _configService.Current.ServerStorage[idx]; + } + catch + { + _configService.Current.CurrentServer = 0; + EnsureMainExists(); + return CurrentServer!; + } + } + + public string[] GetServerNames() + { + return _configService.Current.ServerStorage.Select(v => v.ServerName).ToArray(); + } + + public bool HasValidConfig() + { + return CurrentServer != null && CurrentServer.SecretKeys.Any(); + } + + public void Save() + { + var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; + _logger.LogDebug("{caller} Calling config save", caller); + _configService.Save(); + } + + public void SelectServer(int idx) + { + _configService.Current.CurrentServer = idx; + CurrentServer!.FullPause = false; + Save(); + } + + internal void AddCurrentCharacterToServer(int serverSelectionIndex = -1, bool save = true) + { + if (serverSelectionIndex == -1) serverSelectionIndex = CurrentServerIndex; + var server = GetServerByIndex(serverSelectionIndex); + if (server.Authentications.Any(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), StringComparison.Ordinal) + && c.WorldId == _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult())) + return; + + server.Authentications.Add(new Authentication() + { + CharacterName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), + WorldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(), + SecretKeyIdx = server.SecretKeys.Last().Key, + }); + + if (save) + Save(); + } + + internal void AddEmptyCharacterToServer(int serverSelectionIndex) + { + var server = GetServerByIndex(serverSelectionIndex); + server.Authentications.Add(new Authentication() + { + SecretKeyIdx = server.SecretKeys.Any() ? server.SecretKeys.First().Key : -1, + }); + Save(); + } + + internal void AddOpenPairTag(string tag) + { + CurrentServerTagStorage().OpenPairTags.Add(tag); + _serverTagConfig.Save(); + } + + internal void AddServer(ServerStorage serverStorage) + { + _configService.Current.ServerStorage.Add(serverStorage); + Save(); + } + + internal void AddTag(string tag) + { + CurrentServerTagStorage().ServerAvailablePairTags.Add(tag); + _serverTagConfig.Save(); + } + + internal void AddTagForUid(string uid, string tagName) + { + if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) + { + tags.Add(tagName); + } + else + { + CurrentServerTagStorage().UidServerPairedUserTags[uid] = [tagName]; + } + + _serverTagConfig.Save(); + } + + internal bool ContainsOpenPairTag(string tag) + { + return CurrentServerTagStorage().OpenPairTags.Contains(tag); + } + + internal bool ContainsTag(string uid, string tag) + { + if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) + { + return tags.Contains(tag, StringComparer.Ordinal); + } + + return false; + } + + internal void DeleteServer(ServerStorage selectedServer) + { + if (Array.IndexOf(_configService.Current.ServerStorage.ToArray(), selectedServer) < + _configService.Current.CurrentServer) + { + _configService.Current.CurrentServer--; + } + + _configService.Current.ServerStorage.Remove(selectedServer); + Save(); + } + + internal string? GetNoteForGid(string gID) + { + if (CurrentNotesStorage().GidServerComments.TryGetValue(gID, out var note)) + { + if (string.IsNullOrEmpty(note)) return null; + return note; + } + + return null; + } + + internal string? GetNoteForUid(string uid) + { + if (CurrentNotesStorage().UidServerComments.TryGetValue(uid, out var note)) + { + if (string.IsNullOrEmpty(note)) return null; + return note; + } + return null; + } + + internal string? GetNameForUid(string uid) + { + if (CurrentNotesStorage().UidLastSeenNames.TryGetValue(uid, out var name)) + { + if (string.IsNullOrEmpty(name)) return null; + return name; + } + return null; + } + + internal HashSet GetServerAvailablePairTags() + { + return CurrentServerTagStorage().ServerAvailablePairTags; + } + + internal ShellConfig GetShellConfigForGid(string gid) + { + if (CurrentSyncshellStorage().GidShellConfig.TryGetValue(gid, out var config)) + return config; + + // Pick the next higher syncshell number that is available + int newShellNumber = CurrentSyncshellStorage().GidShellConfig.Count > 0 ? CurrentSyncshellStorage().GidShellConfig.Select(x => x.Value.ShellNumber).Max() + 1 : 1; + + var shellConfig = new ShellConfig{ + ShellNumber = newShellNumber + }; + + // Save config to avoid auto-generated numbers shuffling around + SaveShellConfigForGid(gid, shellConfig); + + return CurrentSyncshellStorage().GidShellConfig[gid]; + } + + internal int GetShellNumberForGid(string gid) + { + return GetShellConfigForGid(gid).ShellNumber; + } + + internal Dictionary> GetUidServerPairedUserTags() + { + return CurrentServerTagStorage().UidServerPairedUserTags; + } + + internal HashSet GetUidsForTag(string tag) + { + return CurrentServerTagStorage().UidServerPairedUserTags.Where(p => p.Value.Contains(tag, StringComparer.Ordinal)).Select(p => p.Key).ToHashSet(StringComparer.Ordinal); + } + + internal bool HasTags(string uid) + { + if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) + { + return tags.Any(); + } + + return false; + } + + internal void RemoveCharacterFromServer(int serverSelectionIndex, Authentication item) + { + var server = GetServerByIndex(serverSelectionIndex); + server.Authentications.Remove(item); + Save(); + } + + internal void RemoveOpenPairTag(string tag) + { + CurrentServerTagStorage().OpenPairTags.Remove(tag); + _serverTagConfig.Save(); + } + + internal void RemoveTag(string tag) + { + CurrentServerTagStorage().ServerAvailablePairTags.Remove(tag); + foreach (var uid in GetUidsForTag(tag)) + { + RemoveTagForUid(uid, tag, save: false); + } + _serverTagConfig.Save(); + } + + internal void RemoveTagForUid(string uid, string tagName, bool save = true) + { + if (CurrentServerTagStorage().UidServerPairedUserTags.TryGetValue(uid, out var tags)) + { + tags.Remove(tagName); + + if (save) + { + _serverTagConfig.Save(); + } + } + } + + internal void RenameTag(string oldName, string newName) + { + CurrentServerTagStorage().ServerAvailablePairTags.Remove(oldName); + CurrentServerTagStorage().ServerAvailablePairTags.Add(newName); + foreach (var existingTags in CurrentServerTagStorage().UidServerPairedUserTags.Select(k => k.Value)) + { + if (existingTags.Remove(oldName)) + existingTags.Add(newName); + } + } + + internal void SaveNotes() + { + _notesConfig.Save(); + } + + internal void SetNoteForGid(string gid, string note, bool save = true) + { + if (string.IsNullOrEmpty(gid)) return; + + CurrentNotesStorage().GidServerComments[gid] = note; + if (save) + _notesConfig.Save(); + } + + internal void SetNoteForUid(string uid, string note, bool save = true) + { + if (string.IsNullOrEmpty(uid)) return; + + CurrentNotesStorage().UidServerComments[uid] = note; + if (save) + _notesConfig.Save(); + } + + internal void SetNameForUid(string uid, string name) + { + if (string.IsNullOrEmpty(uid)) return; + + if (CurrentNotesStorage().UidLastSeenNames.TryGetValue(uid, out var currentName) && currentName.Equals(name, StringComparison.Ordinal)) + return; + + CurrentNotesStorage().UidLastSeenNames[uid] = name; + _notesConfig.Save(); + } + + internal void SaveShellConfigForGid(string gid, ShellConfig config) + { + if (string.IsNullOrEmpty(gid)) return; + + // This is somewhat pointless because ShellConfig is a ref type we returned to the caller anyway... + CurrentSyncshellStorage().GidShellConfig[gid] = config; + + _syncshellConfig.Save(); + } + + internal bool IsUidWhitelisted(string uid) + { + _cachedWhitelistedUIDs ??= [.. CurrentBlockStorage().Whitelist]; + return _cachedWhitelistedUIDs.Contains(uid); + } + + internal bool IsUidBlacklisted(string uid) + { + _cachedBlacklistedUIDs ??= [.. CurrentBlockStorage().Blacklist]; + return _cachedBlacklistedUIDs.Contains(uid); + } + + internal void AddWhitelistUid(string uid) + { + if (IsUidWhitelisted(uid)) + return; + if (CurrentBlockStorage().Blacklist.RemoveAll(u => u.Equals(uid, StringComparison.Ordinal)) > 0) + _cachedBlacklistedUIDs = null; + CurrentBlockStorage().Whitelist.Add(uid); + _cachedWhitelistedUIDs = null; + _blockConfig.Save(); + } + + internal void AddBlacklistUid(string uid) + { + if (IsUidBlacklisted(uid)) + return; + if (CurrentBlockStorage().Whitelist.RemoveAll(u => u.Equals(uid, StringComparison.Ordinal)) > 0) + _cachedWhitelistedUIDs = null; + CurrentBlockStorage().Blacklist.Add(uid); + _cachedBlacklistedUIDs = null; + _blockConfig.Save(); + } + + internal void RemoveWhitelistUid(string uid) + { + if (CurrentBlockStorage().Whitelist.RemoveAll(u => u.Equals(uid, StringComparison.Ordinal)) > 0) + _cachedWhitelistedUIDs = null; + _blockConfig.Save(); + } + + internal void RemoveBlacklistUid(string uid) + { + if (CurrentBlockStorage().Blacklist.RemoveAll(u => u.Equals(uid, StringComparison.Ordinal)) > 0) + _cachedBlacklistedUIDs = null; + _blockConfig.Save(); + } + + private ServerNotesStorage CurrentNotesStorage() + { + TryCreateCurrentNotesStorage(); + return _notesConfig.Current.ServerNotes[CurrentApiUrl]; + } + + private ServerTagStorage CurrentServerTagStorage() + { + TryCreateCurrentServerTagStorage(); + return _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl]; + } + + private ServerShellStorage CurrentSyncshellStorage() + { + TryCreateCurrentSyncshellStorage(); + return _syncshellConfig.Current.ServerShellStorage[CurrentApiUrl]; + } + + private ServerBlockStorage CurrentBlockStorage() + { + TryCreateCurrentBlockStorage(); + return _blockConfig.Current.ServerBlocks[CurrentApiUrl]; + } + + private void EnsureMainExists() + { + bool elfExists = false; + for (int i = 0; i < _configService.Current.ServerStorage.Count; ++i) + { + var x = _configService.Current.ServerStorage[i]; + if (x.ServerUri.Equals(ApiController.SnowcloakServiceUri, StringComparison.OrdinalIgnoreCase)) + elfExists = true; + } + if (!elfExists) + { + _logger.LogDebug("Re-adding missing server {uri}", ApiController.SnowcloakServiceUri); + _configService.Current.ServerStorage.Insert(0, new ServerStorage() { ServerUri = ApiController.SnowcloakServiceUri, ServerName = ApiController.SnowcloakServer }); + if (_configService.Current.CurrentServer >= 0) + _configService.Current.CurrentServer++; + } + Save(); + } + + private void TryCreateCurrentNotesStorage() + { + if (!_notesConfig.Current.ServerNotes.ContainsKey(CurrentApiUrl)) + { + _notesConfig.Current.ServerNotes[CurrentApiUrl] = new(); + } + } + + private void TryCreateCurrentServerTagStorage() + { + if (!_serverTagConfig.Current.ServerTagStorage.ContainsKey(CurrentApiUrl)) + { + _serverTagConfig.Current.ServerTagStorage[CurrentApiUrl] = new(); + } + } + + private void TryCreateCurrentSyncshellStorage() + { + if (!_syncshellConfig.Current.ServerShellStorage.ContainsKey(CurrentApiUrl)) + { + _syncshellConfig.Current.ServerShellStorage[CurrentApiUrl] = new(); + } + } + + private void TryCreateCurrentBlockStorage() + { + if (!_blockConfig.Current.ServerBlocks.ContainsKey(CurrentApiUrl)) + { + _blockConfig.Current.ServerBlocks[CurrentApiUrl] = new(); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/UiFactory.cs b/MareSynchronos/Services/UiFactory.cs new file mode 100644 index 0000000..c518d4e --- /dev/null +++ b/MareSynchronos/Services/UiFactory.cs @@ -0,0 +1,60 @@ +using MareSynchronos.API.Dto.Group; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI; +using MareSynchronos.UI.Components.Popup; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public class UiFactory +{ + private readonly ILoggerFactory _loggerFactory; + private readonly MareMediator _mareMediator; + private readonly ApiController _apiController; + private readonly UiSharedService _uiSharedService; + private readonly PairManager _pairManager; + private readonly ServerConfigurationManager _serverConfigManager; + private readonly MareProfileManager _mareProfileManager; + private readonly PerformanceCollectorService _performanceCollectorService; + + public UiFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, ApiController apiController, + UiSharedService uiSharedService, PairManager pairManager, ServerConfigurationManager serverConfigManager, + MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService) + { + _loggerFactory = loggerFactory; + _mareMediator = mareMediator; + _apiController = apiController; + _uiSharedService = uiSharedService; + _pairManager = pairManager; + _serverConfigManager = serverConfigManager; + _mareProfileManager = mareProfileManager; + _performanceCollectorService = performanceCollectorService; + } + + public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto) + { + return new SyncshellAdminUI(_loggerFactory.CreateLogger(), _mareMediator, + _apiController, _uiSharedService, _pairManager, dto, _performanceCollectorService); + } + + public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair) + { + return new StandaloneProfileUi(_loggerFactory.CreateLogger(), _mareMediator, + _uiSharedService, _serverConfigManager, _mareProfileManager, _pairManager, pair, _performanceCollectorService); + } + + public PermissionWindowUI CreatePermissionPopupUi(Pair pair) + { + return new PermissionWindowUI(_loggerFactory.CreateLogger(), pair, + _mareMediator, _uiSharedService, _apiController, _performanceCollectorService); + } + + public PlayerAnalysisUI CreatePlayerAnalysisUi(Pair pair) + { + return new PlayerAnalysisUI(_loggerFactory.CreateLogger(), pair, + _mareMediator, _uiSharedService, _performanceCollectorService); + } +} diff --git a/MareSynchronos/Services/UiService.cs b/MareSynchronos/Services/UiService.cs new file mode 100644 index 0000000..aa38c3b --- /dev/null +++ b/MareSynchronos/Services/UiService.cs @@ -0,0 +1,137 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Windowing; +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services.Mediator; +using MareSynchronos.UI; +using MareSynchronos.UI.Components.Popup; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Services; + +public sealed class UiService : DisposableMediatorSubscriberBase +{ + private readonly List _createdWindows = []; + private readonly IUiBuilder _uiBuilder; + private readonly FileDialogManager _fileDialogManager; + private readonly ILogger _logger; + private readonly MareConfigService _mareConfigService; + private readonly WindowSystem _windowSystem; + private readonly UiFactory _uiFactory; + + public UiService(ILogger logger, IUiBuilder uiBuilder, + MareConfigService mareConfigService, WindowSystem windowSystem, + IEnumerable windows, + UiFactory uiFactory, FileDialogManager fileDialogManager, + MareMediator mareMediator) : base(logger, mareMediator) + { + _logger = logger; + _logger.LogTrace("Creating {type}", GetType().Name); + _uiBuilder = uiBuilder; + _mareConfigService = mareConfigService; + _windowSystem = windowSystem; + _uiFactory = uiFactory; + _fileDialogManager = fileDialogManager; + + _uiBuilder.DisableGposeUiHide = true; + _uiBuilder.Draw += Draw; + _uiBuilder.OpenConfigUi += ToggleUi; + _uiBuilder.OpenMainUi += ToggleMainUi; + + foreach (var window in windows) + { + _windowSystem.AddWindow(window); + } + + Mediator.Subscribe(this, (msg) => + { + if (!_createdWindows.Exists(p => p is StandaloneProfileUi ui + && string.Equals(ui.Pair.UserData.AliasOrUID, msg.Pair.UserData.AliasOrUID, StringComparison.Ordinal))) + { + var window = _uiFactory.CreateStandaloneProfileUi(msg.Pair); + _createdWindows.Add(window); + _windowSystem.AddWindow(window); + } + }); + + Mediator.Subscribe(this, (msg) => + { + if (!_createdWindows.Exists(p => p is SyncshellAdminUI ui + && string.Equals(ui.GroupFullInfo.GID, msg.GroupInfo.GID, StringComparison.Ordinal))) + { + var window = _uiFactory.CreateSyncshellAdminUi(msg.GroupInfo); + _createdWindows.Add(window); + _windowSystem.AddWindow(window); + } + }); + + Mediator.Subscribe(this, (msg) => + { + if (!_createdWindows.Exists(p => p is PermissionWindowUI ui + && msg.Pair == ui.Pair)) + { + var window = _uiFactory.CreatePermissionPopupUi(msg.Pair); + _createdWindows.Add(window); + _windowSystem.AddWindow(window); + } + }); + + Mediator.Subscribe(this, (msg) => + { + if (!_createdWindows.Exists(p => p is PlayerAnalysisUI ui + && msg.Pair == ui.Pair)) + { + var window = _uiFactory.CreatePlayerAnalysisUi(msg.Pair); + _createdWindows.Add(window); + _windowSystem.AddWindow(window); + } + }); + + Mediator.Subscribe(this, (msg) => + { + _windowSystem.RemoveWindow(msg.Window); + _createdWindows.Remove(msg.Window); + msg.Window.Dispose(); + }); + } + + public void ToggleMainUi() + { + if (_mareConfigService.Current.HasValidSetup()) + Mediator.Publish(new UiToggleMessage(typeof(CompactUi))); + else + Mediator.Publish(new UiToggleMessage(typeof(IntroUi))); + } + + public void ToggleUi() + { + if (_mareConfigService.Current.HasValidSetup()) + Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))); + else + Mediator.Publish(new UiToggleMessage(typeof(IntroUi))); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _logger.LogTrace("Disposing {type}", GetType().Name); + + _windowSystem.RemoveAllWindows(); + + foreach (var window in _createdWindows) + { + window.Dispose(); + } + + _uiBuilder.Draw -= Draw; + _uiBuilder.OpenConfigUi -= ToggleUi; + _uiBuilder.OpenMainUi -= ToggleMainUi; + } + + private void Draw() + { + _windowSystem.Draw(); + _fileDialogManager.Draw(); + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/VisibilityService.cs b/MareSynchronos/Services/VisibilityService.cs new file mode 100644 index 0000000..2731c17 --- /dev/null +++ b/MareSynchronos/Services/VisibilityService.cs @@ -0,0 +1,105 @@ +using MareSynchronos.Interop.Ipc; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace MareSynchronos.Services; + +// Detect when players of interest are visible +public class VisibilityService : DisposableMediatorSubscriberBase +{ + private enum TrackedPlayerStatus + { + NotVisible, + Visible, + MareHandled + }; + + private readonly DalamudUtilService _dalamudUtil; + private readonly ConcurrentDictionary _trackedPlayerVisibility = new(StringComparer.Ordinal); + private readonly List _makeVisibleNextFrame = new(); + private readonly IpcCallerMare _mare; + private readonly HashSet cachedMareAddresses = new(); + private uint _cachedAddressSum = 0; + private uint _cachedAddressSumDebounce = 1; + + public VisibilityService(ILogger logger, MareMediator mediator, IpcCallerMare mare, DalamudUtilService dalamudUtil) + : base(logger, mediator) + { + _mare = mare; + _dalamudUtil = dalamudUtil; + Mediator.Subscribe(this, (_) => FrameworkUpdate()); + } + + public void StartTracking(string ident) + { + _trackedPlayerVisibility.TryAdd(ident, TrackedPlayerStatus.NotVisible); + } + + public void StopTracking(string ident) + { + // No PairVisibilityMessage is emitted if the player was visible when removed + _trackedPlayerVisibility.TryRemove(ident, out _); + } + + private void FrameworkUpdate() + { + var mareHandledAddresses = _mare.GetHandledGameAddresses(); + uint addressSum = 0; + + foreach (var addr in mareHandledAddresses) + addressSum ^= (uint)addr.GetHashCode(); + + if (addressSum != _cachedAddressSum) + { + if (addressSum == _cachedAddressSumDebounce) + { + cachedMareAddresses.Clear(); + foreach (var addr in mareHandledAddresses) + cachedMareAddresses.Add(addr); + _cachedAddressSum = addressSum; + } + else + { + _cachedAddressSumDebounce = addressSum; + } + } + + foreach (var player in _trackedPlayerVisibility) + { + string ident = player.Key; + var findResult = _dalamudUtil.FindPlayerByNameHash(ident); + var isMareHandled = cachedMareAddresses.Contains(findResult.Address); + var isVisible = findResult.ObjectId != 0 && !isMareHandled; + + if (player.Value == TrackedPlayerStatus.MareHandled && !isMareHandled) + _trackedPlayerVisibility.TryUpdate(ident, newValue: TrackedPlayerStatus.NotVisible, comparisonValue: TrackedPlayerStatus.MareHandled); + + if (player.Value == TrackedPlayerStatus.NotVisible && isVisible) + { + if (_makeVisibleNextFrame.Contains(ident)) + { + if (_trackedPlayerVisibility.TryUpdate(ident, newValue: TrackedPlayerStatus.Visible, comparisonValue: TrackedPlayerStatus.NotVisible)) + Mediator.Publish(new(ident, IsVisible: true)); + } + else + _makeVisibleNextFrame.Add(ident); + } + else if (player.Value == TrackedPlayerStatus.NotVisible && isMareHandled) + { + // Send a technically redundant visibility update with the added intent of triggering PairHandler to undo the application by name + if (_trackedPlayerVisibility.TryUpdate(ident, newValue: TrackedPlayerStatus.MareHandled, comparisonValue: TrackedPlayerStatus.NotVisible)) + Mediator.Publish(new(ident, IsVisible: false, Invalidate: true)); + } + else if (player.Value == TrackedPlayerStatus.Visible && !isVisible) + { + var newTrackedStatus = isMareHandled ? TrackedPlayerStatus.MareHandled : TrackedPlayerStatus.NotVisible; + if (_trackedPlayerVisibility.TryUpdate(ident, newValue: newTrackedStatus, comparisonValue: TrackedPlayerStatus.Visible)) + Mediator.Publish(new(ident, IsVisible: false, Invalidate: isMareHandled)); + } + + if (!isVisible) + _makeVisibleNextFrame.Remove(ident); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/XivDataAnalyzer.cs b/MareSynchronos/Services/XivDataAnalyzer.cs new file mode 100644 index 0000000..27b8841 --- /dev/null +++ b/MareSynchronos/Services/XivDataAnalyzer.cs @@ -0,0 +1,257 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.Havok.Animation; +using FFXIVClientStructs.Havok.Common.Base.Types; +using FFXIVClientStructs.Havok.Common.Serialize.Util; +using Lumina.Data; +using MareSynchronos.FileCache; +using MareSynchronos.Interop.GameModel; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Handlers; +using Microsoft.Extensions.Logging; +using System.Runtime.InteropServices; + +namespace MareSynchronos.Services; + +public sealed class XivDataAnalyzer +{ + private readonly ILogger _logger; + private readonly FileCacheManager _fileCacheManager; + private readonly XivDataStorageService _configService; + private readonly List _failedCalculatedTris = []; + private readonly List _failedCalculatedTex = []; + + public XivDataAnalyzer(ILogger logger, FileCacheManager fileCacheManager, + XivDataStorageService configService) + { + _logger = logger; + _fileCacheManager = fileCacheManager; + _configService = configService; + } + + public unsafe Dictionary>? GetSkeletonBoneIndices(GameObjectHandler handler) + { + if (handler.Address == nint.Zero) return null; + var chara = (CharacterBase*)(((Character*)handler.Address)->GameObject.DrawObject); + if (chara->GetModelType() != CharacterBase.ModelType.Human) return null; + var resHandles = chara->Skeleton->SkeletonResourceHandles; + Dictionary> outputIndices = []; + try + { + for (int i = 0; i < chara->Skeleton->PartialSkeletonCount; i++) + { + var handle = *(resHandles + i); + _logger.LogTrace("Iterating over SkeletonResourceHandle #{i}:{x}", i, ((nint)handle).ToString("X")); + if ((nint)handle == nint.Zero) continue; + var curBones = handle->BoneCount; + // this is unrealistic, the filename shouldn't ever be that long + if (handle->FileName.Length > 1024) continue; + var skeletonName = handle->FileName.ToString(); + if (string.IsNullOrEmpty(skeletonName)) continue; + outputIndices[skeletonName] = new(); + for (ushort boneIdx = 0; boneIdx < curBones; boneIdx++) + { + var boneName = handle->HavokSkeleton->Bones[boneIdx].Name.String; + if (boneName == null) continue; + outputIndices[skeletonName].Add((ushort)(boneIdx + 1)); + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not process skeleton data"); + } + + return (outputIndices.Count != 0 && outputIndices.Values.All(u => u.Count > 0)) ? outputIndices : null; + } + + public unsafe Dictionary>? GetBoneIndicesFromPap(string hash) + { + if (_configService.Current.BonesDictionary.TryGetValue(hash, out var bones)) return bones; + + var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash, preferSubst: true); + if (cacheEntity == null) return null; + + using BinaryReader reader = new BinaryReader(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 + reader.ReadInt32(); // ignore + reader.ReadInt16(); // read 2 (num animations) + reader.ReadInt16(); // read 2 (modelid) + var type = reader.ReadByte();// read 1 (type) + if (type != 0) return null; // it's not human, just ignore it, whatever + + reader.ReadByte(); // read 1 (variant) + reader.ReadInt32(); // ignore + var havokPosition = reader.ReadInt32(); + var footerPosition = reader.ReadInt32(); + var havokDataSize = footerPosition - havokPosition; + reader.BaseStream.Position = havokPosition; + var havokData = reader.ReadBytes(havokDataSize); + if (havokData.Length <= 8) return null; // no havok data + + var output = new Dictionary>(StringComparer.OrdinalIgnoreCase); + var tempHavokDataPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + ".hkx"; + var tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath); + + try + { + File.WriteAllBytes(tempHavokDataPath, havokData); + + var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1]; + loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry(); + loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry(); + loadoptions->Flags = new hkFlags + { + Storage = (int)(hkSerializeUtil.LoadOptionBits.Default) + }; + + var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions); + if (resource == null) + { + throw new InvalidOperationException("Resource was null after loading"); + } + + var rootLevelName = @"hkRootLevelContainer"u8; + fixed (byte* n1 = rootLevelName) + { + var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry()); + var animationName = @"hkaAnimationContainer"u8; + fixed (byte* n2 = animationName) + { + var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null); + for (int i = 0; i < animContainer->Bindings.Length; i++) + { + var binding = animContainer->Bindings[i].ptr; + var boneTransform = binding->TransformTrackToBoneIndices; + string name = binding->OriginalSkeletonName.String! + "_" + i; + output[name] = []; + for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++) + { + output[name].Add((ushort)boneTransform[boneIdx]); + } + output[name].Sort(); + } + + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath); + } + finally + { + Marshal.FreeHGlobal(tempHavokDataPathAnsi); + File.Delete(tempHavokDataPath); + } + + _configService.Current.BonesDictionary[hash] = output; + _configService.Save(); + return output; + } + + public long GetTrianglesByHash(string hash) + { + if (_configService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0) + return cachedTris; + + if (_failedCalculatedTris.Contains(hash, StringComparer.Ordinal)) + return 0; + + var path = _fileCacheManager.GetFileCacheByHash(hash, preferSubst: true); + if (path == null || !path.ResolvedFilepath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)) + return 0; + + var filePath = path.ResolvedFilepath; + + try + { + _logger.LogDebug("Detected Model File {path}, calculating Tris", filePath); + var file = new MdlFile(filePath); + if (file.LodCount <= 0) + { + _failedCalculatedTris.Add(hash); + _configService.Current.TriangleDictionary[hash] = 0; + _configService.Save(); + return 0; + } + + long tris = 0; + for (int i = 0; i < file.LodCount; i++) + { + try + { + var meshIdx = file.Lods[i].MeshIndex; + var meshCnt = file.Lods[i].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); + continue; + } + + if (tris > 0) + { + _logger.LogDebug("TriAnalysis: {filePath} => {tris} triangles", filePath, tris); + _configService.Current.TriangleDictionary[hash] = tris; + _configService.Save(); + break; + } + } + + return tris; + } + catch (Exception e) + { + _failedCalculatedTris.Add(hash); + _configService.Current.TriangleDictionary[hash] = 0; + _configService.Save(); + _logger.LogWarning(e, "Could not parse file {file}", filePath); + return 0; + } + } + + public (uint Format, int MipCount, ushort Width, ushort Height) GetTexFormatByHash(string hash) + { + if (_configService.Current.TexDictionary.TryGetValue(hash, out var cachedTex) && cachedTex.Mip0Size > 0) + return cachedTex; + + if (_failedCalculatedTex.Contains(hash, StringComparer.Ordinal)) + return default; + + var path = _fileCacheManager.GetFileCacheByHash(hash); + if (path == null || !path.ResolvedFilepath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) + return default; + + var filePath = path.ResolvedFilepath; + + try + { + _logger.LogDebug("Detected Texture File {path}, reading header", filePath); + using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + using var r = new LuminaBinaryReader(stream); + var texHeader = r.ReadStructure(); + + if (texHeader.Format == default || texHeader.MipCount == 0 || texHeader.ArraySize != 0 || texHeader.MipCount > 13) + { + _failedCalculatedTex.Add(hash); + _configService.Current.TexDictionary[hash] = default; + _configService.Save(); + return default; + } + + return ((uint)texHeader.Format, texHeader.MipCount, texHeader.Width, texHeader.Height); + } + catch (Exception e) + { + _failedCalculatedTex.Add(hash); + _configService.Current.TriangleDictionary[hash] = 0; + _configService.Save(); + _logger.LogWarning(e, "Could not parse file {file}", filePath); + return default; + } + } +} diff --git a/MareSynchronos/Snowcloak.json b/MareSynchronos/Snowcloak.json new file mode 100644 index 0000000..39b04e3 --- /dev/null +++ b/MareSynchronos/Snowcloak.json @@ -0,0 +1,14 @@ +{ + "Author": "Eauldane", + "Name": "Snowcloak", + "Punchline": "Share your true self.", + "Description": "This plugin will synchronize your Penumbra mods and current Glamourer state with other paired clients automatically.", + "InternalName": "Snowcloak", + "ApplicableVersion": "any", + "Tags": [ + "customization" + ], + "IconUrl": "https://raw.githubusercontent.com/Eauldane/SnowcloakClient/refs/heads/main/MareSynchronos/images/logo.png", + "RepoUrl": "https://github.com/Eauldane/SnowcloakClient", + "CanUnloadAsync": true +} diff --git a/MareSynchronos/UI/CharaDataHubUi.Functions.cs b/MareSynchronos/UI/CharaDataHubUi.Functions.cs new file mode 100644 index 0000000..3f2c809 --- /dev/null +++ b/MareSynchronos/UI/CharaDataHubUi.Functions.cs @@ -0,0 +1,196 @@ +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services.CharaData.Models; +using System.Text; + +namespace MareSynchronos.UI; + +internal sealed partial class CharaDataHubUi +{ + private static string GetAccessTypeString(AccessTypeDto dto) => dto switch + { + AccessTypeDto.AllPairs => "All Pairs", + AccessTypeDto.ClosePairs => "Direct Pairs", + AccessTypeDto.Individuals => "Specified", + AccessTypeDto.Public => "Everyone", + _ => ((int)dto).ToString() + }; + + private static string GetShareTypeString(ShareTypeDto dto) => dto switch + { + ShareTypeDto.Private => "Code Only", + ShareTypeDto.Shared => "Shared", + _ => ((int)dto).ToString() + }; + + private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry) + { + if (!poseEntry.HasWorldData) return "This Pose has no world data attached."; + return poseEntry.WorldDataDescriptor; + } + + + private void GposeMetaInfoAction(Action gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning) + { + StringBuilder sb = new StringBuilder(); + + sb.AppendLine(actionDescription); + bool isDisabled = false; + + void AddErrorStart(StringBuilder sb) + { + sb.Append(UiSharedService.TooltipSeparator); + sb.AppendLine("Cannot execute:"); + } + + if (dto == null) + { + if (!isDisabled) AddErrorStart(sb); + sb.AppendLine("- No metainfo present"); + isDisabled = true; + } + if (!dto?.CanBeDownloaded ?? false) + { + if (!isDisabled) AddErrorStart(sb); + sb.AppendLine("- Character is not downloadable"); + isDisabled = true; + } + if (!_uiSharedService.IsInGpose) + { + if (!isDisabled) AddErrorStart(sb); + sb.AppendLine("- Requires to be in GPose"); + isDisabled = true; + } + if (!hasValidGposeTarget && !isSpawning) + { + if (!isDisabled) AddErrorStart(sb); + sb.AppendLine("- Requires a valid GPose target"); + isDisabled = true; + } + if (isSpawning && !_charaDataManager.BrioAvailable) + { + if (!isDisabled) AddErrorStart(sb); + sb.AppendLine("- Requires Brio to be installed."); + isDisabled = true; + } + + using (ImRaii.Group()) + { + using var dis = ImRaii.Disabled(isDisabled); + gposeActionDraw.Invoke(dto); + } + if (sb.Length > 0) + { + UiSharedService.AttachToolTip(sb.ToString()); + } + } + + private void GposePoseAction(Action poseActionDraw, string poseDescription, bool hasValidGposeTarget) + { + StringBuilder sb = new StringBuilder(); + + sb.AppendLine(poseDescription); + bool isDisabled = false; + + void AddErrorStart(StringBuilder sb) + { + sb.Append(UiSharedService.TooltipSeparator); + sb.AppendLine("Cannot execute:"); + } + + if (!_uiSharedService.IsInGpose) + { + if (!isDisabled) AddErrorStart(sb); + sb.AppendLine("- Requires to be in GPose"); + isDisabled = true; + } + if (!hasValidGposeTarget) + { + if (!isDisabled) AddErrorStart(sb); + sb.AppendLine("- Requires a valid GPose target"); + isDisabled = true; + } + if (!_charaDataManager.BrioAvailable) + { + if (!isDisabled) AddErrorStart(sb); + sb.AppendLine("- Requires Brio to be installed."); + isDisabled = true; + } + + using (ImRaii.Group()) + { + using var dis = ImRaii.Disabled(isDisabled); + poseActionDraw.Invoke(); + } + if (sb.Length > 0) + { + UiSharedService.AttachToolTip(sb.ToString()); + } + } + + private void SetWindowSizeConstraints(bool? inGposeTab = null) + { + SizeConstraints = new() + { + MinimumSize = new((inGposeTab ?? false) ? 400 : 1000, 500), + MaximumSize = new((inGposeTab ?? false) ? 400 : 1000, 2000) + }; + } + + private void UpdateFilteredFavorites() + { + _ = Task.Run(async () => + { + if (_charaDataManager.DownloadMetaInfoTask != null) + { + await _charaDataManager.DownloadMetaInfoTask.ConfigureAwait(false); + } + Dictionary newFiltered = []; + foreach (var favorite in _configService.Current.FavoriteCodes) + { + var uid = favorite.Key.Split(":")[0]; + var note = _serverConfigurationManager.GetNoteForUid(uid) ?? string.Empty; + bool hasMetaInfo = _charaDataManager.TryGetMetaInfo(favorite.Key, out var metaInfo); + bool addFavorite = + (string.IsNullOrEmpty(_filterCodeNote) + || (note.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase) + || uid.Contains(_filterCodeNote, StringComparison.OrdinalIgnoreCase))) + && (string.IsNullOrEmpty(_filterDescription) + || (favorite.Value.CustomDescription.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase) + || (metaInfo != null && metaInfo!.Description.Contains(_filterDescription, StringComparison.OrdinalIgnoreCase)))) + && (!_filterPoseOnly + || (metaInfo != null && metaInfo!.HasPoses)) + && (!_filterWorldOnly + || (metaInfo != null && metaInfo!.HasWorldData)); + if (addFavorite) + { + newFiltered[favorite.Key] = (favorite.Value, metaInfo, hasMetaInfo); + } + } + + _filteredFavorites = newFiltered; + }); + } + + private void UpdateFilteredItems() + { + if (_charaDataManager.GetSharedWithYouTask == null) + { + _filteredDict = _charaDataManager.SharedWithYouData + .SelectMany(k => k.Value) + .Where(k => + (!_sharedWithYouDownloadableFilter || k.CanBeDownloaded) + && (string.IsNullOrEmpty(_sharedWithYouDescriptionFilter) || k.Description.Contains(_sharedWithYouDescriptionFilter, StringComparison.OrdinalIgnoreCase))) + .GroupBy(k => k.Uploader) + .ToDictionary(k => + { + var note = _serverConfigurationManager.GetNoteForUid(k.Key.UID); + if (note == null) return k.Key.AliasOrUID; + return $"{note} ({k.Key.AliasOrUID})"; + }, k => k.ToList(), StringComparer.OrdinalIgnoreCase) + .Where(k => (string.IsNullOrEmpty(_sharedWithYouOwnerFilter) || k.Key.Contains(_sharedWithYouOwnerFilter, StringComparison.OrdinalIgnoreCase))) + .OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToDictionary(); + } + } +} diff --git a/MareSynchronos/UI/CharaDataHubUi.GposeTogether.cs b/MareSynchronos/UI/CharaDataHubUi.GposeTogether.cs new file mode 100644 index 0000000..f5c4059 --- /dev/null +++ b/MareSynchronos/UI/CharaDataHubUi.GposeTogether.cs @@ -0,0 +1,227 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.Services.CharaData.Models; + +namespace MareSynchronos.UI; + +internal sealed partial class CharaDataHubUi +{ + private string _joinLobbyId = string.Empty; + private void DrawGposeTogether() + { + if (!_charaDataManager.BrioAvailable) + { + ImGuiHelpers.ScaledDummy(5); + UiSharedService.DrawGroupedCenteredColorText("BRIO IS MANDATORY FOR GPOSE TOGETHER.", ImGuiColors.DalamudRed); + ImGuiHelpers.ScaledDummy(5); + } + + if (!_uiSharedService.ApiController.IsConnected) + { + ImGuiHelpers.ScaledDummy(5); + UiSharedService.DrawGroupedCenteredColorText("CANNOT USE GPOSE TOGETHER WHILE DISCONNECTED FROM THE SERVER.", ImGuiColors.DalamudRed); + ImGuiHelpers.ScaledDummy(5); + } + + _uiSharedService.BigText("GPose Together"); + DrawHelpFoldout("GPose together is a way to do multiplayer GPose sessions and collaborations." + UiSharedService.DoubleNewLine + + "GPose together requires Brio to function. Only Brio is also supported for the actual posing interactions. Attempting to pose using other tools will lead to conflicts and exploding characters." + UiSharedService.DoubleNewLine + + "To use GPose together you either create or join a GPose Together Lobby. After you and other people have joined, make sure that everyone is on the same map. " + + "It is not required for you to be on the same server, DC or instance. Users that are on the same map will be drawn as moving purple wisps in the overworld, so you can easily find each other." + UiSharedService.DoubleNewLine + + "Once you are close to each other you can initiate GPose. You must either assign or spawn characters for each of the lobby users. Their own poses and positions to their character will be automatically applied." + Environment.NewLine + + "Pose and location data during GPose are updated approximately every few seconds."); + + using var disabled = ImRaii.Disabled(!_charaDataManager.BrioAvailable || !_uiSharedService.ApiController.IsConnected); + + UiSharedService.DistanceSeparator(); + _uiSharedService.BigText("Lobby Controls"); + if (string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Create New GPose Together Lobby")) + { + _charaDataGposeTogetherManager.CreateNewLobby(); + } + ImGuiHelpers.ScaledDummy(5); + ImGui.SetNextItemWidth(250); + ImGui.InputTextWithHint("##lobbyId", "GPose Lobby Id", ref _joinLobbyId, 30); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Join GPose Together Lobby")) + { + _charaDataGposeTogetherManager.JoinGPoseLobby(_joinLobbyId); + _joinLobbyId = string.Empty; + } + if (!string.IsNullOrEmpty(_charaDataGposeTogetherManager.LastGPoseLobbyId) + && _uiSharedService.IconTextButton(FontAwesomeIcon.LongArrowAltRight, $"Rejoin Last Lobby {_charaDataGposeTogetherManager.LastGPoseLobbyId}")) + { + _charaDataGposeTogetherManager.JoinGPoseLobby(_charaDataGposeTogetherManager.LastGPoseLobbyId); + } + } + else + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("GPose Lobby"); + ImGui.SameLine(); + UiSharedService.ColorTextWrapped(_charaDataGposeTogetherManager.CurrentGPoseLobbyId, ImGuiColors.ParsedGreen); + ImGui.SameLine(); + if (_uiSharedService.IconButton(FontAwesomeIcon.Clipboard)) + { + ImGui.SetClipboardText(_charaDataGposeTogetherManager.CurrentGPoseLobbyId); + } + UiSharedService.AttachToolTip("Copy Lobby ID to clipboard."); + using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowLeft, "Leave GPose Lobby")) + { + _charaDataGposeTogetherManager.LeaveGPoseLobby(); + } + } + UiSharedService.AttachToolTip("Leave the current GPose lobby." + UiSharedService.TooltipSeparator + "Hold CTRL and click to leave."); + } + UiSharedService.DistanceSeparator(); + using (ImRaii.Disabled(string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId))) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowUp, "Send Updated Character Data")) + { + _ = _charaDataGposeTogetherManager.PushCharacterDownloadDto(); + } + UiSharedService.AttachToolTip("This will send your current appearance, pose and world data to all users in the lobby."); + if (!_uiSharedService.IsInGpose) + { + ImGuiHelpers.ScaledDummy(5); + UiSharedService.DrawGroupedCenteredColorText("Assigning users to characters is only available in GPose.", ImGuiColors.DalamudYellow, 300); + } + UiSharedService.DistanceSeparator(); + ImGui.TextUnformatted("Users In Lobby"); + var gposeCharas = _dalamudUtilService.GetGposeCharactersFromObjectTable(); + var self = _dalamudUtilService.GetPlayerCharacter(); + gposeCharas = gposeCharas.Where(c => c != null && !string.Equals(c.Name.TextValue, self.Name.TextValue, StringComparison.Ordinal)).ToList(); + + using (ImRaii.Child("charaChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize)) + { + ImGuiHelpers.ScaledDummy(3); + + if (!_charaDataGposeTogetherManager.UsersInLobby.Any() && !string.IsNullOrEmpty(_charaDataGposeTogetherManager.CurrentGPoseLobbyId)) + { + UiSharedService.DrawGroupedCenteredColorText("No other users in current GPose lobby", ImGuiColors.DalamudYellow); + } + else + { + foreach (var user in _charaDataGposeTogetherManager.UsersInLobby) + { + DrawLobbyUser(user, gposeCharas); + } + } + } + } + } + + private void DrawLobbyUser(GposeLobbyUserData user, + IEnumerable gposeCharas) + { + using var id = ImRaii.PushId(user.UserData.UID); + using var indent = ImRaii.PushIndent(5f); + var sameMapAndServer = _charaDataGposeTogetherManager.IsOnSameMapAndServer(user); + var width = ImGui.GetContentRegionAvail().X - 5; + UiSharedService.DrawGrouped(() => + { + var availWidth = ImGui.GetContentRegionAvail().X; + ImGui.AlignTextToFramePadding(); + var note = _serverConfigurationManager.GetNoteForUid(user.UserData.UID); + var userText = note == null ? user.UserData.AliasOrUID : $"{note} ({user.UserData.AliasOrUID})"; + UiSharedService.ColorText(userText, ImGuiColors.ParsedGreen); + + var buttonsize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.ArrowRight).X; + var buttonsize2 = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X; + ImGui.SameLine(); + ImGui.SetCursorPosX(availWidth - (buttonsize + buttonsize2 + ImGui.GetStyle().ItemSpacing.X)); + using (ImRaii.Disabled(!_uiSharedService.IsInGpose || user.CharaData == null || user.Address == nint.Zero)) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.ArrowRight)) + { + _ = _charaDataGposeTogetherManager.ApplyCharaData(user); + } + } + UiSharedService.AttachToolTip("Apply newly received character data to selected actor." + UiSharedService.TooltipSeparator + "Note: If the button is grayed out, the latest data has already been applied."); + ImGui.SameLine(); + using (ImRaii.Disabled(!_uiSharedService.IsInGpose || user.CharaData == null || sameMapAndServer.SameEverything)) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) + { + _ = _charaDataGposeTogetherManager.SpawnAndApplyData(user); + } + } + UiSharedService.AttachToolTip("Spawn new actor, apply character data and and assign it to this user." + UiSharedService.TooltipSeparator + "Note: If the button is grayed out, " + + "the user has not sent any character data or you are on the same map, server and instance. If the latter is the case, join a group with that user and assign the character to them."); + + + using (ImRaii.Group()) + { + UiSharedService.ColorText("Map Info", ImGuiColors.DalamudGrey); + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.ExternalLinkSquareAlt, ImGuiColors.DalamudGrey); + } + UiSharedService.AttachToolTip(user.WorldDataDescriptor + UiSharedService.TooltipSeparator); + + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && user.WorldData != null) + { + _dalamudUtilService.SetMarkerAndOpenMap(new(user.WorldData.Value.PositionX, user.WorldData.Value.PositionY, user.WorldData.Value.PositionZ), user.Map); + } + UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same map." : "You are not on the same map.") + UiSharedService.TooltipSeparator + + "Note: Click to open the users location on your map." + Environment.NewLine + + "Note: For GPose synchronization to work properly, you must be on the same map."); + + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed); + UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same server." : "You are not on the same server.") + UiSharedService.TooltipSeparator + + "Note: GPose synchronization is not dependent on the current server, but you will have to spawn a character for the other lobby users."); + + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed); + UiSharedService.AttachToolTip(sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not the same instanced area." + UiSharedService.TooltipSeparator + + "Note: Users not in your instance, but on the same map, will be drawn as floating wisps." + Environment.NewLine + + "Note: GPose synchronization is not dependent on the current instance, but you will have to spawn a character for the other lobby users."); + + using (ImRaii.Disabled(!_uiSharedService.IsInGpose)) + { + ImGui.SetNextItemWidth(200); + using (var combo = ImRaii.Combo("##character", string.IsNullOrEmpty(user.AssociatedCharaName) ? "No character assigned" : CharaName(user.AssociatedCharaName))) + { + if (combo) + { + foreach (var chara in gposeCharas) + { + if (chara == null) continue; + + if (ImGui.Selectable(CharaName(chara.Name.TextValue), chara.Address == user.Address)) + { + user.AssociatedCharaName = chara.Name.TextValue; + user.Address = chara.Address; + } + } + } + } + ImGui.SameLine(); + using (ImRaii.Disabled(user.Address == nint.Zero)) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Trash)) + { + user.AssociatedCharaName = string.Empty; + user.Address = nint.Zero; + } + } + UiSharedService.AttachToolTip("Unassign Actor for this user"); + if (_uiSharedService.IsInGpose && user.Address == nint.Zero) + { + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudRed); + UiSharedService.AttachToolTip("No valid character assigned for this user. Pose data will not be applied."); + } + } + }, 5, width); + ImGuiHelpers.ScaledDummy(5); + } +} diff --git a/MareSynchronos/UI/CharaDataHubUi.McdOnline.cs b/MareSynchronos/UI/CharaDataHubUi.McdOnline.cs new file mode 100644 index 0000000..3cc29c2 --- /dev/null +++ b/MareSynchronos/UI/CharaDataHubUi.McdOnline.cs @@ -0,0 +1,851 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.Services.CharaData.Models; +using System.Numerics; + +namespace MareSynchronos.UI; + +internal sealed partial class CharaDataHubUi +{ + private void DrawEditCharaData(CharaDataFullExtendedDto? dataDto) + { + using var imguiid = ImRaii.PushId(dataDto?.Id ?? "NoData"); + + if (dataDto == null) + { + ImGuiHelpers.ScaledDummy(5); + UiSharedService.DrawGroupedCenteredColorText("Select an entry above to edit its data.", ImGuiColors.DalamudYellow); + return; + } + + var updateDto = _charaDataManager.GetUpdateDto(dataDto.Id); + + if (updateDto == null) + { + UiSharedService.DrawGroupedCenteredColorText("Something went awfully wrong and there's no update DTO. Try updating Character Data via the button above.", ImGuiColors.DalamudYellow); + return; + } + + bool canUpdate = updateDto.HasChanges; + if (canUpdate || _charaDataManager.CharaUpdateTask != null) + { + ImGuiHelpers.ScaledDummy(5); + } + + var indent = ImRaii.PushIndent(10f); + if (canUpdate || _charaDataManager.UploadTask != null) + { + ImGuiHelpers.ScaledDummy(5); + UiSharedService.DrawGrouped(() => + { + if (canUpdate) + { + ImGui.AlignTextToFramePadding(); + UiSharedService.ColorTextWrapped("Warning: You have unsaved changes!", ImGuiColors.DalamudRed); + ImGui.SameLine(); + using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Save to Server")) + { + _charaDataManager.UploadCharaData(dataDto.Id); + } + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Undo, "Undo all changes")) + { + updateDto.UndoChanges(); + } + } + if (_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted) + { + UiSharedService.ColorTextWrapped("Updating data on server, please wait.", ImGuiColors.DalamudYellow); + } + } + + if (!_charaDataManager.UploadTask?.IsCompleted ?? false) + { + DisableDisabled(() => + { + if (_charaDataManager.UploadProgress != null) + { + UiSharedService.ColorTextWrapped(_charaDataManager.UploadProgress.Value ?? string.Empty, ImGuiColors.DalamudYellow); + } + if ((!_charaDataManager.UploadTask?.IsCompleted ?? false) && _uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Cancel Upload")) + { + _charaDataManager.CancelUpload(); + } + else if (_charaDataManager.UploadTask?.IsCompleted ?? false) + { + var color = UiSharedService.GetBoolColor(_charaDataManager.UploadTask.Result.Success); + UiSharedService.ColorTextWrapped(_charaDataManager.UploadTask.Result.Output, color); + } + }); + } + else if (_charaDataManager.UploadTask?.IsCompleted ?? false) + { + var color = UiSharedService.GetBoolColor(_charaDataManager.UploadTask.Result.Success); + UiSharedService.ColorTextWrapped(_charaDataManager.UploadTask.Result.Output, color); + } + }); + } + indent.Dispose(); + + if (canUpdate || _charaDataManager.CharaUpdateTask != null) + { + ImGuiHelpers.ScaledDummy(5); + } + + using var child = ImRaii.Child("editChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize); + + DrawEditCharaDataGeneral(dataDto, updateDto); + ImGuiHelpers.ScaledDummy(5); + DrawEditCharaDataAccessAndSharing(updateDto); + ImGuiHelpers.ScaledDummy(5); + DrawEditCharaDataAppearance(dataDto, updateDto); + ImGuiHelpers.ScaledDummy(5); + DrawEditCharaDataPoses(updateDto); + } + + private void DrawEditCharaDataAccessAndSharing(CharaDataExtendedUpdateDto updateDto) + { + _uiSharedService.BigText("Access and Sharing"); + + ImGui.SetNextItemWidth(200); + var dtoAccessType = updateDto.AccessType; + if (ImGui.BeginCombo("Access Restrictions", GetAccessTypeString(dtoAccessType))) + { + foreach (var accessType in Enum.GetValues(typeof(AccessTypeDto)).Cast()) + { + if (ImGui.Selectable(GetAccessTypeString(accessType), accessType == dtoAccessType)) + { + updateDto.AccessType = accessType; + } + } + + ImGui.EndCombo(); + } + _uiSharedService.DrawHelpText("You can control who has access to your character data based on the access restrictions." + UiSharedService.TooltipSeparator + + "Specified: Only people and syncshells you directly specify in 'Specific Individuals / Syncshells' can access this character data" + Environment.NewLine + + "Direct Pairs: Only people you have directly paired can access this character data" + Environment.NewLine + + "All Pairs: All people you have paired can access this character data" + Environment.NewLine + + "Everyone: Everyone can access this character data" + UiSharedService.TooltipSeparator + + "Note: To access your character data the person in question requires to have the code. Exceptions for 'Shared' data, see 'Sharing' below." + Environment.NewLine + + "Note: For 'Direct' and 'All Pairs' the pause state plays a role. Paused people will not be able to access your character data." + Environment.NewLine + + "Note: Directly specified Individuals or Syncshells in the 'Specific Individuals / Syncshells' list will be able to access your character data regardless of pause or pair state."); + + DrawSpecific(updateDto); + + ImGui.SetNextItemWidth(200); + var dtoShareType = updateDto.ShareType; + using (ImRaii.Disabled(dtoAccessType == AccessTypeDto.Public)) + { + if (ImGui.BeginCombo("Sharing", GetShareTypeString(dtoShareType))) + { + foreach (var shareType in Enum.GetValues(typeof(ShareTypeDto)).Cast()) + { + if (ImGui.Selectable(GetShareTypeString(shareType), shareType == dtoShareType)) + { + updateDto.ShareType = shareType; + } + } + + ImGui.EndCombo(); + } + } + _uiSharedService.DrawHelpText("This regulates how you want to distribute this character data." + UiSharedService.TooltipSeparator + + "Code Only: People require to have the code to download this character data" + Environment.NewLine + + "Shared: People that are allowed through 'Access Restrictions' will have this character data entry displayed in 'Shared with You' (it can also be accessed through the code)" + UiSharedService.TooltipSeparator + + "Note: Shared is incompatible with Access Restriction 'Everyone'"); + + ImGuiHelpers.ScaledDummy(10f); + } + + private void DrawEditCharaDataAppearance(CharaDataFullExtendedDto dataDto, CharaDataExtendedUpdateDto updateDto) + { + _uiSharedService.BigText("Appearance"); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Set Appearance to Current Appearance")) + { + _charaDataManager.SetAppearanceData(dataDto.Id); + } + _uiSharedService.DrawHelpText("This will overwrite the appearance data currently stored in this Character Data entry with your current appearance."); + ImGui.SameLine(); + using (ImRaii.Disabled(dataDto.HasMissingFiles || !updateDto.IsAppearanceEqual || _charaDataManager.DataApplicationTask != null)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.CheckCircle, "Preview Saved Apperance on Self")) + { + _charaDataManager.ApplyDataToSelf(dataDto); + } + } + _uiSharedService.DrawHelpText("This will download and apply the saved character data to yourself. Once loaded it will automatically revert itself within 15 seconds." + UiSharedService.TooltipSeparator + + "Note: Weapons will not be displayed correctly unless using the same job as the saved data."); + + ImGui.TextUnformatted("Contains Glamourer Data"); + ImGui.SameLine(); + bool hasGlamourerdata = !string.IsNullOrEmpty(updateDto.GlamourerData); + ImGui.SameLine(200); + _uiSharedService.BooleanToColoredIcon(hasGlamourerdata, false); + + ImGui.TextUnformatted("Contains Files"); + var hasFiles = (updateDto.FileGamePaths ?? []).Any() || (dataDto.OriginalFiles.Any()); + ImGui.SameLine(200); + _uiSharedService.BooleanToColoredIcon(hasFiles, false); + if (hasFiles && updateDto.IsAppearanceEqual) + { + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(20, 1); + ImGui.SameLine(); + var pos = ImGui.GetCursorPosX(); + ImGui.NewLine(); + ImGui.SameLine(pos); + ImGui.TextUnformatted($"{dataDto.FileGamePaths.DistinctBy(k => k.HashOrFileSwap).Count()} unique file hashes (original upload: {dataDto.OriginalFiles.DistinctBy(k => k.HashOrFileSwap).Count()} file hashes)"); + ImGui.NewLine(); + ImGui.SameLine(pos); + ImGui.TextUnformatted($"{dataDto.FileGamePaths.Count} associated game paths"); + ImGui.NewLine(); + ImGui.SameLine(pos); + ImGui.TextUnformatted($"{dataDto.FileSwaps!.Count} file swaps"); + ImGui.NewLine(); + ImGui.SameLine(pos); + if (!dataDto.HasMissingFiles) + { + UiSharedService.ColorTextWrapped("All files to download this character data are present on the server", ImGuiColors.HealerGreen); + } + else + { + UiSharedService.ColorTextWrapped($"{dataDto.MissingFiles.DistinctBy(k => k.HashOrFileSwap).Count()} files to download this character data are missing on the server", ImGuiColors.DalamudRed); + ImGui.NewLine(); + ImGui.SameLine(pos); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Attempt to upload missing files and restore Character Data")) + { + _charaDataManager.UploadMissingFiles(dataDto.Id); + } + } + } + else if (hasFiles && !updateDto.IsAppearanceEqual) + { + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(20, 1); + ImGui.SameLine(); + UiSharedService.ColorTextWrapped("New data was set. It may contain files that require to be uploaded (will happen on Saving to server)", ImGuiColors.DalamudYellow); + } + + ImGui.TextUnformatted("Contains Manipulation Data"); + bool hasManipData = !string.IsNullOrEmpty(updateDto.ManipulationData); + ImGui.SameLine(200); + _uiSharedService.BooleanToColoredIcon(hasManipData, false); + + ImGui.TextUnformatted("Contains Customize+ Data"); + ImGui.SameLine(); + bool hasCustomizeData = !string.IsNullOrEmpty(updateDto.CustomizeData); + ImGui.SameLine(200); + _uiSharedService.BooleanToColoredIcon(hasCustomizeData, false); + } + + private void DrawEditCharaDataGeneral(CharaDataFullExtendedDto dataDto, CharaDataExtendedUpdateDto updateDto) + { + _uiSharedService.BigText("General"); + string code = dataDto.FullId; + using (ImRaii.Disabled()) + { + ImGui.SetNextItemWidth(200); + ImGui.InputText("##CharaDataCode", ref code, 255, ImGuiInputTextFlags.ReadOnly); + } + ImGui.SameLine(); + ImGui.TextUnformatted("Chara Data Code"); + ImGui.SameLine(); + if (_uiSharedService.IconButton(FontAwesomeIcon.Copy)) + { + ImGui.SetClipboardText(code); + } + UiSharedService.AttachToolTip("Copy Code to Clipboard"); + + string creationTime = dataDto.CreatedDate.ToLocalTime().ToString(); + string updateTime = dataDto.UpdatedDate.ToLocalTime().ToString(); + string downloadCount = dataDto.DownloadCount.ToString(); + using (ImRaii.Disabled()) + { + ImGui.SetNextItemWidth(200); + ImGui.InputText("##CreationDate", ref creationTime, 255, ImGuiInputTextFlags.ReadOnly); + } + ImGui.SameLine(); + ImGui.TextUnformatted("Creation Date"); + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(20); + ImGui.SameLine(); + using (ImRaii.Disabled()) + { + ImGui.SetNextItemWidth(200); + ImGui.InputText("##LastUpdate", ref updateTime, 255, ImGuiInputTextFlags.ReadOnly); + } + ImGui.SameLine(); + ImGui.TextUnformatted("Last Update Date"); + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(23); + ImGui.SameLine(); + using (ImRaii.Disabled()) + { + ImGui.SetNextItemWidth(50); + ImGui.InputText("##DlCount", ref downloadCount, 255, ImGuiInputTextFlags.ReadOnly); + } + ImGui.SameLine(); + ImGui.TextUnformatted("Download Count"); + + string description = updateDto.Description; + ImGui.SetNextItemWidth(735); + if (ImGui.InputText("##Description", ref description, 200)) + { + updateDto.Description = description; + } + ImGui.SameLine(); + ImGui.TextUnformatted("Description"); + _uiSharedService.DrawHelpText("Description for this Character Data." + UiSharedService.TooltipSeparator + + "Note: the description will be visible to anyone who can access this character data. See 'Access Restrictions' and 'Sharing' below."); + + var expiryDate = updateDto.ExpiryDate; + bool isExpiring = expiryDate != DateTime.MaxValue; + if (ImGui.Checkbox("Expires", ref isExpiring)) + { + updateDto.SetExpiry(isExpiring); + } + _uiSharedService.DrawHelpText("If expiration is enabled, the uploaded character data will be automatically deleted from the server at the specified date."); + using (ImRaii.Disabled(!isExpiring)) + { + ImGui.SameLine(); + ImGui.SetNextItemWidth(100); + if (ImGui.BeginCombo("Year", expiryDate.Year.ToString())) + { + for (int year = DateTime.UtcNow.Year; year < DateTime.UtcNow.Year + 4; year++) + { + if (ImGui.Selectable(year.ToString(), year == expiryDate.Year)) + { + updateDto.SetExpiry(year, expiryDate.Month, expiryDate.Day); + } + } + ImGui.EndCombo(); + } + ImGui.SameLine(); + + int daysInMonth = DateTime.DaysInMonth(expiryDate.Year, expiryDate.Month); + ImGui.SetNextItemWidth(100); + if (ImGui.BeginCombo("Month", expiryDate.Month.ToString())) + { + for (int month = 1; month <= 12; month++) + { + if (ImGui.Selectable(month.ToString(), month == expiryDate.Month)) + { + updateDto.SetExpiry(expiryDate.Year, month, expiryDate.Day); + } + } + ImGui.EndCombo(); + } + ImGui.SameLine(); + + ImGui.SetNextItemWidth(100); + if (ImGui.BeginCombo("Day", expiryDate.Day.ToString())) + { + for (int day = 1; day <= daysInMonth; day++) + { + if (ImGui.Selectable(day.ToString(), day == expiryDate.Day)) + { + updateDto.SetExpiry(expiryDate.Year, expiryDate.Month, day); + } + } + ImGui.EndCombo(); + } + } + ImGuiHelpers.ScaledDummy(5); + + using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Character Data")) + { + _ = _charaDataManager.DeleteCharaData(dataDto); + SelectedDtoId = string.Empty; + } + } + if (!UiSharedService.CtrlPressed()) + { + UiSharedService.AttachToolTip("Hold CTRL and click to delete the current data. This operation is irreversible."); + } + } + + private void DrawEditCharaDataPoses(CharaDataExtendedUpdateDto updateDto) + { + _uiSharedService.BigText("Poses"); + var poseCount = updateDto.PoseList.Count(); + using (ImRaii.Disabled(poseCount >= maxPoses)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add new Pose")) + { + updateDto.AddPose(); + } + } + ImGui.SameLine(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, poseCount == maxPoses)) + ImGui.TextUnformatted($"{poseCount}/{maxPoses} poses attached"); + ImGuiHelpers.ScaledDummy(5); + + using var indent = ImRaii.PushIndent(10f); + int poseNumber = 1; + + if (!_uiSharedService.IsInGpose && _charaDataManager.BrioAvailable) + { + ImGuiHelpers.ScaledDummy(5); + UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data you need to be in GPose.", ImGuiColors.DalamudYellow); + ImGuiHelpers.ScaledDummy(5); + } + else if (!_charaDataManager.BrioAvailable) + { + ImGuiHelpers.ScaledDummy(5); + UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data Brio requires to be installed.", ImGuiColors.DalamudRed); + ImGuiHelpers.ScaledDummy(5); + } + + foreach (var pose in updateDto.PoseList) + { + ImGui.AlignTextToFramePadding(); + using var id = ImRaii.PushId("pose" + poseNumber); + ImGui.TextUnformatted(poseNumber.ToString()); + + if (pose.Id == null) + { + ImGui.SameLine(50); + _uiSharedService.IconText(FontAwesomeIcon.Plus, ImGuiColors.DalamudYellow); + UiSharedService.AttachToolTip("This pose has not been added to the server yet. Save changes to upload this Pose data."); + } + + bool poseHasChanges = updateDto.PoseHasChanges(pose); + if (poseHasChanges) + { + ImGui.SameLine(50); + _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow); + UiSharedService.AttachToolTip("This pose has changes that have not been saved to the server yet."); + } + + ImGui.SameLine(75); + if (pose.Description == null && pose.WorldData == null && pose.PoseData == null) + { + UiSharedService.ColorText("Pose scheduled for deletion", ImGuiColors.DalamudYellow); + } + else + { + var desc = pose.Description ?? string.Empty; + if (ImGui.InputTextWithHint("##description", "Description", ref desc, 100)) + { + pose.Description = desc; + updateDto.UpdatePoseList(); + } + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete")) + { + updateDto.RemovePose(pose); + } + + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(10, 1); + ImGui.SameLine(); + bool hasPoseData = !string.IsNullOrEmpty(pose.PoseData); + _uiSharedService.IconText(FontAwesomeIcon.Running, UiSharedService.GetBoolColor(hasPoseData)); + UiSharedService.AttachToolTip(hasPoseData + ? "This Pose entry has pose data attached" + : "This Pose entry has no pose data attached"); + ImGui.SameLine(); + + using (ImRaii.Disabled(!_uiSharedService.IsInGpose || !(_charaDataManager.AttachingPoseTask?.IsCompleted ?? true) || !_charaDataManager.BrioAvailable)) + { + using var poseid = ImRaii.PushId("poseSet" + poseNumber); + if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) + { + _charaDataManager.AttachPoseData(pose, updateDto); + } + UiSharedService.AttachToolTip("Apply current pose data to pose"); + } + ImGui.SameLine(); + using (ImRaii.Disabled(!hasPoseData)) + { + using var poseid = ImRaii.PushId("poseDelete" + poseNumber); + if (_uiSharedService.IconButton(FontAwesomeIcon.Trash)) + { + pose.PoseData = string.Empty; + updateDto.UpdatePoseList(); + } + UiSharedService.AttachToolTip("Delete current pose data from pose"); + } + + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(10, 1); + ImGui.SameLine(); + var worldData = pose.WorldData ?? default; + bool hasWorldData = worldData != default; + _uiSharedService.IconText(FontAwesomeIcon.Globe, UiSharedService.GetBoolColor(hasWorldData)); + var tooltipText = !hasWorldData ? "This Pose has no world data attached." : "This Pose has world data attached."; + if (hasWorldData) + { + tooltipText += UiSharedService.TooltipSeparator + "Click to show location on map"; + } + UiSharedService.AttachToolTip(tooltipText); + if (hasWorldData && ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + _dalamudUtilService.SetMarkerAndOpenMap(position: new Vector3(worldData.PositionX, worldData.PositionY, worldData.PositionZ), + _dalamudUtilService.MapData.Value[worldData.LocationInfo.MapId].Map); + } + ImGui.SameLine(); + using (ImRaii.Disabled(!_uiSharedService.IsInGpose || !(_charaDataManager.AttachingPoseTask?.IsCompleted ?? true) || !_charaDataManager.BrioAvailable)) + { + using var worldId = ImRaii.PushId("worldSet" + poseNumber); + if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) + { + _charaDataManager.AttachWorldData(pose, updateDto); + } + UiSharedService.AttachToolTip("Apply current world position data to pose"); + } + ImGui.SameLine(); + using (ImRaii.Disabled(!hasWorldData)) + { + using var worldId = ImRaii.PushId("worldDelete" + poseNumber); + if (_uiSharedService.IconButton(FontAwesomeIcon.Trash)) + { + pose.WorldData = default(WorldData); + updateDto.UpdatePoseList(); + } + UiSharedService.AttachToolTip("Delete current world position data from pose"); + } + } + + if (poseHasChanges) + { + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Undo, "Undo")) + { + updateDto.RevertDeletion(pose); + } + } + + poseNumber++; + } + } + + private void DrawMcdOnline() + { + _uiSharedService.BigText("Online Character Data"); + + DrawHelpFoldout("In this tab you can create, view and edit your own Character Data that is stored on the server." + Environment.NewLine + Environment.NewLine + + "Character Data Online functions similar to the previous MCDF standard for exporting your character, except that you do not have to send a file to the other person but solely a code." + Environment.NewLine + Environment.NewLine + + "There would be a bit too much to explain here on what you can do here in its entirety, however, all elements in this tab have help texts attached what they are used for. Please review them carefully." + Environment.NewLine + Environment.NewLine + + "Be mindful that when you share your Character Data with other people there is a chance that, with the help of unsanctioned 3rd party plugins, your appearance could be stolen irreversibly, just like when using MCDF."); + + ImGuiHelpers.ScaledDummy(5); + using (ImRaii.Disabled((!_charaDataManager.GetAllDataTask?.IsCompleted ?? false) + || (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted))) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Download your Online Character Data from Server")) + { + _ = _charaDataManager.GetAllData(_disposalCts.Token); + } + } + if (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted) + { + UiSharedService.AttachToolTip("You can only refresh all character data from server every minute. Please wait."); + } + + using (var table = ImRaii.Table("Own Character Data", 12, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY, + new Vector2(ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X, 110))) + { + if (table) + { + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 18); + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 18); + ImGui.TableSetupColumn("Code"); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Created"); + ImGui.TableSetupColumn("Updated"); + ImGui.TableSetupColumn("Download Count", ImGuiTableColumnFlags.WidthFixed, 18); + ImGui.TableSetupColumn("Downloadable", ImGuiTableColumnFlags.WidthFixed, 18); + ImGui.TableSetupColumn("Files", ImGuiTableColumnFlags.WidthFixed, 32); + ImGui.TableSetupColumn("Glamourer", ImGuiTableColumnFlags.WidthFixed, 18); + ImGui.TableSetupColumn("Customize+", ImGuiTableColumnFlags.WidthFixed, 18); + ImGui.TableSetupColumn("Expires", ImGuiTableColumnFlags.WidthFixed, 18); + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + foreach (var entry in _charaDataManager.OwnCharaData.Values.OrderBy(b => b.CreatedDate)) + { + var uDto = _charaDataManager.GetUpdateDto(entry.Id); + ImGui.TableNextColumn(); + if (string.Equals(entry.Id, SelectedDtoId, StringComparison.Ordinal)) + _uiSharedService.IconText(FontAwesomeIcon.CaretRight); + + ImGui.TableNextColumn(); + DrawAddOrRemoveFavorite(entry); + + ImGui.TableNextColumn(); + var idText = entry.FullId; + if (uDto?.HasChanges ?? false) + { + UiSharedService.ColorText(idText, ImGuiColors.DalamudYellow); + UiSharedService.AttachToolTip("This entry has unsaved changes"); + } + else + { + ImGui.TextUnformatted(idText); + } + if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id; + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.Description); + if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id; + UiSharedService.AttachToolTip(entry.Description); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.CreatedDate.ToLocalTime().ToString()); + if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id; + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.UpdatedDate.ToLocalTime().ToString()); + if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id; + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.DownloadCount.ToString()); + if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id; + + ImGui.TableNextColumn(); + bool isDownloadable = !entry.HasMissingFiles + && !string.IsNullOrEmpty(entry.GlamourerData); + _uiSharedService.BooleanToColoredIcon(isDownloadable, false); + if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id; + UiSharedService.AttachToolTip(isDownloadable ? "Can be downloaded by others" : "Cannot be downloaded: Has missing files or data, please review this entry manually"); + + ImGui.TableNextColumn(); + var count = entry.FileGamePaths.Concat(entry.FileSwaps).Count(); + ImGui.TextUnformatted(count.ToString()); + if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id; + UiSharedService.AttachToolTip(count == 0 ? "No File data attached" : "Has File data attached"); + + ImGui.TableNextColumn(); + bool hasGlamourerData = !string.IsNullOrEmpty(entry.GlamourerData); + _uiSharedService.BooleanToColoredIcon(hasGlamourerData, false); + if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id; + UiSharedService.AttachToolTip(string.IsNullOrEmpty(entry.GlamourerData) ? "No Glamourer data attached" : "Has Glamourer data attached"); + + ImGui.TableNextColumn(); + bool hasCustomizeData = !string.IsNullOrEmpty(entry.CustomizeData); + _uiSharedService.BooleanToColoredIcon(hasCustomizeData, false); + if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id; + UiSharedService.AttachToolTip(string.IsNullOrEmpty(entry.CustomizeData) ? "No Customize+ data attached" : "Has Customize+ data attached"); + + ImGui.TableNextColumn(); + FontAwesomeIcon eIcon = FontAwesomeIcon.None; + if (!Equals(DateTime.MaxValue, entry.ExpiryDate)) + eIcon = FontAwesomeIcon.Clock; + _uiSharedService.IconText(eIcon, ImGuiColors.DalamudYellow); + if (ImGui.IsItemClicked()) SelectedDtoId = entry.Id; + if (eIcon != FontAwesomeIcon.None) + { + UiSharedService.AttachToolTip($"This entry will expire on {entry.ExpiryDate.ToLocalTime()}"); + } + } + } + } + + using (ImRaii.Disabled(!_charaDataManager.Initialized || _charaDataManager.DataCreationTask != null || _charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "New Character Data Entry")) + { + _charaDataManager.CreateCharaDataEntry(_closalCts.Token); + _selectNewEntry = true; + } + } + if (_charaDataManager.DataCreationTask != null) + { + UiSharedService.AttachToolTip("You can only create new character data every few seconds. Please wait."); + } + if (!_charaDataManager.Initialized) + { + UiSharedService.AttachToolTip("Please use the button \"Get Own Chara Data\" once before you can add new data entries."); + } + + if (_charaDataManager.Initialized) + { + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + UiSharedService.TextWrapped($"Chara Data Entries on Server: {_charaDataManager.OwnCharaData.Count}/{_charaDataManager.MaxCreatableCharaData}"); + if (_charaDataManager.OwnCharaData.Count == _charaDataManager.MaxCreatableCharaData) + { + ImGui.AlignTextToFramePadding(); + UiSharedService.ColorTextWrapped("You have reached the maximum Character Data entries and cannot create more.", ImGuiColors.DalamudYellow); + } + } + + if (_charaDataManager.DataCreationTask != null && !_charaDataManager.DataCreationTask.IsCompleted) + { + UiSharedService.ColorTextWrapped("Creating new character data entry on server...", ImGuiColors.DalamudYellow); + } + else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted) + { + var color = _charaDataManager.DataCreationTask.Result.Success ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed; + UiSharedService.ColorTextWrapped(_charaDataManager.DataCreationTask.Result.Output, color); + } + + ImGuiHelpers.ScaledDummy(10); + ImGui.Separator(); + + var charaDataEntries = _charaDataManager.OwnCharaData.Count; + if (charaDataEntries != _dataEntries && _selectNewEntry && _charaDataManager.OwnCharaData.Any()) + { + SelectedDtoId = _charaDataManager.OwnCharaData.OrderBy(o => o.Value.CreatedDate).Last().Value.Id; + _selectNewEntry = false; + } + _dataEntries = _charaDataManager.OwnCharaData.Count; + + _ = _charaDataManager.OwnCharaData.TryGetValue(SelectedDtoId, out var dto); + DrawEditCharaData(dto); + } + + bool _selectNewEntry = false; + int _dataEntries = 0; + + private void DrawSpecific(CharaDataExtendedUpdateDto updateDto) + { + UiSharedService.DrawTree("Access for Specific Individuals / Syncshells", () => + { + using (ImRaii.PushId("user")) + { + using (ImRaii.Group()) + { + InputComboHybrid("##AliasToAdd", "##AliasToAddPicker", ref _specificIndividualAdd, _pairManager.DirectPairs, + static pair => (pair.UserData.UID, pair.UserData.Alias, pair.UserData.AliasOrUID, pair.GetNoteOrName())); + ImGui.SameLine(); + using (ImRaii.Disabled(string.IsNullOrEmpty(_specificIndividualAdd) + || updateDto.UserList.Any(f => string.Equals(f.UID, _specificIndividualAdd, StringComparison.Ordinal) || string.Equals(f.Alias, _specificIndividualAdd, StringComparison.Ordinal)))) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) + { + updateDto.AddUserToList(_specificIndividualAdd); + _specificIndividualAdd = string.Empty; + } + } + ImGui.SameLine(); + ImGui.TextUnformatted("UID/Vanity UID to Add"); + _uiSharedService.DrawHelpText("Users added to this list will be able to access this character data regardless of your pause or pair state with them." + UiSharedService.TooltipSeparator + + "Note: Mistyped entries will be automatically removed on updating data to server."); + + using (var lb = ImRaii.ListBox("Allowed Individuals", new(200, 200))) + { + foreach (var user in updateDto.UserList) + { + var userString = string.IsNullOrEmpty(user.Alias) ? user.UID : $"{user.Alias} ({user.UID})"; + if (ImGui.Selectable(userString, string.Equals(user.UID, _selectedSpecificUserIndividual, StringComparison.Ordinal))) + { + _selectedSpecificUserIndividual = user.UID; + } + } + } + + using (ImRaii.Disabled(string.IsNullOrEmpty(_selectedSpecificUserIndividual))) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove selected User")) + { + updateDto.RemoveUserFromList(_selectedSpecificUserIndividual); + _selectedSpecificUserIndividual = string.Empty; + } + } + } + } + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(20); + ImGui.SameLine(); + + using (ImRaii.PushId("group")) + { + using (ImRaii.Group()) + { + InputComboHybrid("##GroupAliasToAdd", "##GroupAliasToAddPicker", ref _specificGroupAdd, _pairManager.Groups.Keys, + group => (group.GID, group.Alias, group.AliasOrGID, _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)))) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) + { + updateDto.AddGroupToList(_specificGroupAdd); + _specificGroupAdd = string.Empty; + } + } + ImGui.SameLine(); + ImGui.TextUnformatted("GID/Vanity GID to Add"); + _uiSharedService.DrawHelpText("Users in Syncshells added to this list will be able to access this character data regardless of your pause or pair state with them." + UiSharedService.TooltipSeparator + + "Note: Mistyped entries will be automatically removed on updating data to server."); + + using (var lb = ImRaii.ListBox("Allowed Syncshells", new(200, 200))) + { + foreach (var group in updateDto.GroupList) + { + var userString = string.IsNullOrEmpty(group.Alias) ? group.GID : $"{group.Alias} ({group.GID})"; + if (ImGui.Selectable(userString, string.Equals(group.GID, _selectedSpecificGroupIndividual, StringComparison.Ordinal))) + { + _selectedSpecificGroupIndividual = group.GID; + } + } + } + + using (ImRaii.Disabled(string.IsNullOrEmpty(_selectedSpecificGroupIndividual))) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove selected Syncshell")) + { + updateDto.RemoveGroupFromList(_selectedSpecificGroupIndividual); + _selectedSpecificGroupIndividual = string.Empty; + } + } + } + } + + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(5); + }); + } + + private void InputComboHybrid(string inputId, string comboId, ref string value, IEnumerable comboEntries, + Func parseEntry) + { + const float ComponentWidth = 200; + ImGui.SetNextItemWidth(ComponentWidth - ImGui.GetFrameHeight()); + ImGui.InputText(inputId, ref value, 20); + ImGui.SameLine(0.0f, 0.0f); + + using var combo = ImRaii.Combo(comboId, string.Empty, ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft); + if (!combo) + { + return; + } + + if (_openComboHybridEntries is null || !string.Equals(_openComboHybridId, comboId, StringComparison.Ordinal)) + { + var valueSnapshot = value; + _openComboHybridEntries = comboEntries + .Select(parseEntry) + .Where(entry => entry.Id.Contains(valueSnapshot, StringComparison.OrdinalIgnoreCase) + || (entry.Alias is not null && entry.Alias.Contains(valueSnapshot, StringComparison.OrdinalIgnoreCase)) + || (entry.Note is not null && entry.Note.Contains(valueSnapshot, StringComparison.OrdinalIgnoreCase))) + .OrderBy(entry => entry.Note is null ? entry.AliasOrId : $"{entry.Note} ({entry.AliasOrId})", StringComparer.OrdinalIgnoreCase) + .ToArray(); + _openComboHybridId = comboId; + } + _comboHybridUsedLastFrame = true; + + // Is there a better way to handle this? + var width = ComponentWidth - 2 * ImGui.GetStyle().FramePadding.X - (_openComboHybridEntries.Length > 8 ? ImGui.GetStyle().ScrollbarSize : 0); + foreach (var (id, alias, aliasOrId, note) in _openComboHybridEntries) + { + var selected = !string.IsNullOrEmpty(value) + && (string.Equals(id, value, StringComparison.Ordinal) || string.Equals(alias, value, StringComparison.Ordinal)); + using var font = ImRaii.PushFont(UiBuilder.MonoFont, note is null); + if (ImGui.Selectable(note is null ? aliasOrId : $"{note} ({aliasOrId})", selected, ImGuiSelectableFlags.None, new(width, 0))) + { + value = aliasOrId; + } + } + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/CharaDataHubUi.NearbyPoses.cs b/MareSynchronos/UI/CharaDataHubUi.NearbyPoses.cs new file mode 100644 index 0000000..8486375 --- /dev/null +++ b/MareSynchronos/UI/CharaDataHubUi.NearbyPoses.cs @@ -0,0 +1,207 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using System.Numerics; + +namespace MareSynchronos.UI; + +internal partial class CharaDataHubUi +{ + private void DrawNearbyPoses() + { + _uiSharedService.BigText("Poses Nearby"); + + DrawHelpFoldout("This tab will show you all Shared World Poses nearby you." + Environment.NewLine + Environment.NewLine + + "Shared World Poses are poses in character data that have world data attached to them and are set to shared. " + + "This means that all data that is in 'Shared with You' that has a pose with world data attached to it will be shown here if you are nearby." + Environment.NewLine + + "By default all poses that are shared will be shown. Poses taken in housing areas will by default only be shown on the correct world and location." + Environment.NewLine + Environment.NewLine + + "Shared World Poses will appear in the world as floating wisps, as well as in the list below. You can mouse over a Shared World Pose in the list for it to get highlighted in the world." + Environment.NewLine + Environment.NewLine + + "You can apply Shared World Poses to yourself or spawn the associated character to pose with them." + Environment.NewLine + Environment.NewLine + + "You can adjust the filter and change further settings in the 'Settings & Filter' foldout."); + + UiSharedService.DrawTree("Settings & Filters", () => + { + string filterByUser = _charaDataNearbyManager.UserNoteFilter; + if (ImGui.InputTextWithHint("##filterbyuser", "Filter by User", ref filterByUser, 50)) + { + _charaDataNearbyManager.UserNoteFilter = filterByUser; + } + bool onlyCurrent = _configService.Current.NearbyOwnServerOnly; + if (ImGui.Checkbox("Only show Poses on current world", ref onlyCurrent)) + { + _configService.Current.NearbyOwnServerOnly = onlyCurrent; + _configService.Save(); + } + _uiSharedService.DrawHelpText("Show the location of shared Poses with World Data from current world only"); + bool showOwn = _configService.Current.NearbyShowOwnData; + if (ImGui.Checkbox("Also show your own data", ref showOwn)) + { + _configService.Current.NearbyShowOwnData = showOwn; + _configService.Save(); + } + _uiSharedService.DrawHelpText("Show your own Poses as well"); + bool ignoreHousing = _configService.Current.NearbyIgnoreHousingLimitations; + if (ImGui.Checkbox("Ignore Housing Limitations", ref ignoreHousing)) + { + _configService.Current.NearbyIgnoreHousingLimitations = ignoreHousing; + _configService.Save(); + } + _uiSharedService.DrawHelpText("Display all poses in their location regardless of housing limitations. (Ignoring Ward, Plot, Room)" + UiSharedService.TooltipSeparator + + "Note: Poses that utilize housing props, furniture, etc. will not be displayed correctly if not spawned in the right location."); + bool showWisps = _configService.Current.NearbyDrawWisps; + if (ImGui.Checkbox("Show Pose Wisps in the overworld", ref showWisps)) + { + _configService.Current.NearbyDrawWisps = showWisps; + _configService.Save(); + } + _uiSharedService.DrawHelpText("Draw floating wisps where other's poses are in the world."); + int poseDetectionDistance = _configService.Current.NearbyDistanceFilter; + ImGui.SetNextItemWidth(100); + if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000)) + { + _configService.Current.NearbyDistanceFilter = poseDetectionDistance; + _configService.Save(); + } + _uiSharedService.DrawHelpText("Maximum distance in which poses will be shown. Set it to the maximum if you want to see all poses on the current map."); + bool alwaysShow = _configService.Current.NearbyShowAlways; + if (ImGui.Checkbox("Keep active outside Poses Nearby tab", ref alwaysShow)) + { + _configService.Current.NearbyShowAlways = alwaysShow; + _configService.Save(); + } + _uiSharedService.DrawHelpText("Continue the calculation of position of wisps etc. active outside of the 'Poses Nearby' tab." + UiSharedService.TooltipSeparator + + "Note: The wisps etc. will disappear during combat and performing."); + }); + + if (!_uiSharedService.IsInGpose) + { + ImGuiHelpers.ScaledDummy(5); + UiSharedService.DrawGroupedCenteredColorText("Spawning and applying pose data is only available in GPose.", ImGuiColors.DalamudYellow); + ImGuiHelpers.ScaledDummy(5); + } + + DrawUpdateSharedDataButton(); + + UiSharedService.DistanceSeparator(); + + using var child = ImRaii.Child("nearbyPosesChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize); + + ImGuiHelpers.ScaledDummy(3f); + + using var indent = ImRaii.PushIndent(5f); + if (_charaDataNearbyManager.NearbyData.Count == 0) + { + UiSharedService.DrawGroupedCenteredColorText("No Shared World Poses found nearby.", ImGuiColors.DalamudYellow); + } + + bool wasAnythingHovered = false; + int i = 0; + foreach (var pose in _charaDataNearbyManager.NearbyData.OrderBy(v => v.Value.Distance)) + { + using var poseId = ImRaii.PushId("nearbyPose" + (i++)); + var pos = ImGui.GetCursorPos(); + var circleDiameter = 60f; + var circleOriginX = ImGui.GetWindowContentRegionMax().X - circleDiameter - pos.X; + float circleOffsetY = 0; + + UiSharedService.DrawGrouped(() => + { + string? userNote = _serverConfigurationManager.GetNoteForUid(pose.Key.MetaInfo.Uploader.UID); + var noteText = pose.Key.MetaInfo.IsOwnData ? "YOU" : (userNote == null ? pose.Key.MetaInfo.Uploader.AliasOrUID : $"{userNote} ({pose.Key.MetaInfo.Uploader.AliasOrUID})"); + ImGui.TextUnformatted("Pose by"); + ImGui.SameLine(); + UiSharedService.ColorText(noteText, ImGuiColors.ParsedGreen); + using (ImRaii.Group()) + { + UiSharedService.ColorText("Character Data Description", ImGuiColors.DalamudGrey); + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.ExternalLinkAlt, ImGuiColors.DalamudGrey); + } + UiSharedService.AttachToolTip(pose.Key.MetaInfo.Description); + UiSharedService.ColorText("Description", ImGuiColors.DalamudGrey); + ImGui.SameLine(); + UiSharedService.TextWrapped(pose.Key.Description ?? "No Pose Description was set", circleOriginX); + var posAfterGroup = ImGui.GetCursorPos(); + var groupHeightCenter = (posAfterGroup.Y - pos.Y) / 2; + circleOffsetY = (groupHeightCenter - circleDiameter / 2); + if (circleOffsetY < 0) circleOffsetY = 0; + ImGui.SetCursorPos(new Vector2(circleOriginX, pos.Y)); + ImGui.Dummy(new Vector2(circleDiameter, circleDiameter)); + UiSharedService.AttachToolTip("Click to open corresponding map and set map marker" + UiSharedService.TooltipSeparator + + pose.Key.WorldDataDescriptor); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + _dalamudUtilService.SetMarkerAndOpenMap(pose.Key.Position, pose.Key.Map); + } + ImGui.SetCursorPos(posAfterGroup); + if (_uiSharedService.IsInGpose) + { + GposePoseAction(() => + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply Pose")) + { + _charaDataManager.ApplyFullPoseDataToGposeTarget(pose.Key); + } + }, $"Apply pose and position to {CharaName(_gposeTarget)}", _hasValidGposeTarget); + ImGui.SameLine(); + GposeMetaInfoAction((_) => + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Spawn and Pose")) + { + _charaDataManager.SpawnAndApplyWorldTransform(pose.Key.MetaInfo, pose.Key); + } + }, "Spawn actor and apply pose and position", pose.Key.MetaInfo, _hasValidGposeTarget, true); + } + }); + if (ImGui.IsItemHovered()) + { + wasAnythingHovered = true; + _nearbyHovered = pose.Key; + } + var drawList = ImGui.GetWindowDrawList(); + var circleRadius = circleDiameter / 2f; + var windowPos = ImGui.GetWindowPos(); + var scrollX = ImGui.GetScrollX(); + var scrollY = ImGui.GetScrollY(); + var circleCenter = new Vector2(windowPos.X + circleOriginX + circleRadius - scrollX, windowPos.Y + pos.Y + circleRadius + circleOffsetY - scrollY); + var rads = pose.Value.Direction * (Math.PI / 180); + + float halfConeAngleRadians = 15f * (float)Math.PI / 180f; + Vector2 baseDir1 = new Vector2((float)Math.Sin(rads - halfConeAngleRadians), -(float)Math.Cos(rads - halfConeAngleRadians)); + Vector2 baseDir2 = new Vector2((float)Math.Sin(rads + halfConeAngleRadians), -(float)Math.Cos(rads + halfConeAngleRadians)); + + Vector2 coneBase1 = circleCenter + baseDir1 * circleRadius; + Vector2 coneBase2 = circleCenter + baseDir2 * circleRadius; + + // Draw the cone as a filled triangle + drawList.AddTriangleFilled(circleCenter, coneBase1, coneBase2, UiSharedService.Color(ImGuiColors.ParsedGreen)); + drawList.AddCircle(circleCenter, circleDiameter / 2, UiSharedService.Color(ImGuiColors.DalamudWhite), 360, 2); + var distance = pose.Value.Distance.ToString("0.0") + "y"; + var textSize = ImGui.CalcTextSize(distance); + drawList.AddText(new Vector2(circleCenter.X - textSize.X / 2, circleCenter.Y + textSize.Y / 3f), UiSharedService.Color(ImGuiColors.DalamudWhite), distance); + + ImGuiHelpers.ScaledDummy(3); + } + + if (!wasAnythingHovered) _nearbyHovered = null; + _charaDataNearbyManager.SetHoveredVfx(_nearbyHovered); + } + + private void DrawUpdateSharedDataButton() + { + using (ImRaii.Disabled(_charaDataManager.GetAllDataTask != null + || (_charaDataManager.GetSharedWithYouTimeoutTask != null && !_charaDataManager.GetSharedWithYouTimeoutTask.IsCompleted))) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Update Data Shared With You")) + { + _ = _charaDataManager.GetAllSharedData(_disposalCts.Token).ContinueWith(u => UpdateFilteredItems()); + } + } + if (_charaDataManager.GetSharedWithYouTimeoutTask != null && !_charaDataManager.GetSharedWithYouTimeoutTask.IsCompleted) + { + UiSharedService.AttachToolTip("You can only refresh all character data from server every minute. Please wait."); + } + } +} diff --git a/MareSynchronos/UI/CharaDataHubUi.cs b/MareSynchronos/UI/CharaDataHubUi.cs new file mode 100644 index 0000000..bc4da09 --- /dev/null +++ b/MareSynchronos/UI/CharaDataHubUi.cs @@ -0,0 +1,1107 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.CharaData; +using MareSynchronos.Services.CharaData.Models; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.UI; + +internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase +{ + 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 CharaDataGposeTogetherManager _charaDataGposeTogetherManager; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly UiSharedService _uiSharedService; + private CancellationTokenSource _closalCts = new(); + private bool _disableUI = false; + private CancellationTokenSource _disposalCts = new(); + private string _exportDescription = string.Empty; + private string _filterCodeNote = string.Empty; + private string _filterDescription = string.Empty; + private Dictionary>? _filteredDict; + private Dictionary _filteredFavorites = []; + private bool _filterPoseOnly = false; + private bool _filterWorldOnly = false; + private string _gposeTarget = string.Empty; + private bool _hasValidGposeTarget; + private string _importCode = string.Empty; + private bool _isHandlingSelf = false; + private DateTime _lastFavoriteUpdateTime = DateTime.UtcNow; + private PoseEntryExtended? _nearbyHovered; + private bool _openMcdOnlineOnNextRun = false; + private bool _readExport; + private string _selectedDtoId = string.Empty; + private string SelectedDtoId + { + get => _selectedDtoId; + set + { + if (!string.Equals(_selectedDtoId, value, StringComparison.Ordinal)) + { + _charaDataManager.UploadTask = null; + _selectedDtoId = value; + } + + } + } + private string _selectedSpecificUserIndividual = string.Empty; + private string _selectedSpecificGroupIndividual = string.Empty; + private string _sharedWithYouDescriptionFilter = string.Empty; + private bool _sharedWithYouDownloadableFilter = false; + private string _sharedWithYouOwnerFilter = string.Empty; + private string _specificIndividualAdd = string.Empty; + private string _specificGroupAdd = string.Empty; + private bool _abbreviateCharaName = false; + private string? _openComboHybridId = null; + private (string Id, string? Alias, string AliasOrId, string? Note)[]? _openComboHybridEntries = null; + private bool _comboHybridUsedLastFrame = false; + + public CharaDataHubUi(ILogger logger, MareMediator mediator, PerformanceCollectorService performanceCollectorService, + CharaDataManager charaDataManager, CharaDataNearbyManager charaDataNearbyManager, CharaDataConfigService configService, + UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager, + DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager, + CharaDataGposeTogetherManager charaDataGposeTogetherManager) + : base(logger, mediator, "Snowcloak Character Data Hub###SnowcloakCharaDataUI", performanceCollectorService) + { + SetWindowSizeConstraints(); + + _charaDataManager = charaDataManager; + _charaDataNearbyManager = charaDataNearbyManager; + _configService = configService; + _uiSharedService = uiSharedService; + _serverConfigurationManager = serverConfigurationManager; + _dalamudUtilService = dalamudUtilService; + _fileDialogManager = fileDialogManager; + _pairManager = pairManager; + _charaDataGposeTogetherManager = charaDataGposeTogetherManager; + Mediator.Subscribe(this, (_) => IsOpen |= _configService.Current.OpenMareHubOnGposeStart); + Mediator.Subscribe(this, (msg) => + { + IsOpen = true; + _openDataApplicationShared = true; + _sharedWithYouOwnerFilter = msg.UserData.AliasOrUID; + UpdateFilteredItems(); + }); + } + + private bool _openDataApplicationShared = false; + + public string CharaName(string name) + { + if (_abbreviateCharaName) + { + var split = name.Split(" "); + return split[0].First() + ". " + split[1].First() + "."; + } + + return name; + } + + public override void OnClose() + { + if (_disableUI) + { + IsOpen = true; + return; + } + + _closalCts.Cancel(); + SelectedDtoId = string.Empty; + _filteredDict = null; + _sharedWithYouOwnerFilter = string.Empty; + _importCode = string.Empty; + _charaDataNearbyManager.ComputeNearbyData = false; + _openComboHybridId = null; + _openComboHybridEntries = null; + } + + public override void OnOpen() + { + _closalCts = _closalCts.CancelRecreate(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _closalCts.CancelDispose(); + _disposalCts.CancelDispose(); + } + + base.Dispose(disposing); + } + + protected override void DrawInternal() + { + if (!_comboHybridUsedLastFrame) + { + _openComboHybridId = null; + _openComboHybridEntries = null; + } + _comboHybridUsedLastFrame = false; + + _disableUI = !(_charaDataManager.UiBlockingComputation?.IsCompleted ?? true); + if (DateTime.UtcNow.Subtract(_lastFavoriteUpdateTime).TotalSeconds > 2) + { + _lastFavoriteUpdateTime = DateTime.UtcNow; + UpdateFilteredFavorites(); + } + + (_hasValidGposeTarget, _gposeTarget) = _charaDataManager.CanApplyInGpose().GetAwaiter().GetResult(); + + if (!_charaDataManager.BrioAvailable) + { + ImGuiHelpers.ScaledDummy(3); + UiSharedService.DrawGroupedCenteredColorText("To utilize any features related to posing or spawning characters you require to have Brio installed.", ImGuiColors.DalamudRed); + UiSharedService.DistanceSeparator(); + } + + using var disabled = ImRaii.Disabled(_disableUI); + + DisableDisabled(() => + { + if (_charaDataManager.DataApplicationTask != null) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Applying Data to Actor"); + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Cancel Application")) + { + _charaDataManager.CancelDataApplication(); + } + } + if (!string.IsNullOrEmpty(_charaDataManager.DataApplicationProgress)) + { + UiSharedService.ColorTextWrapped(_charaDataManager.DataApplicationProgress, ImGuiColors.DalamudYellow); + } + if (_charaDataManager.DataApplicationTask != null) + { + UiSharedService.ColorTextWrapped("WARNING: During the data application avoid interacting with this actor to prevent potential crashes.", ImGuiColors.DalamudRed); + ImGuiHelpers.ScaledDummy(5); + ImGui.Separator(); + } + }); + + using var tabs = ImRaii.TabBar("TabsTopLevel"); + bool smallUi = false; + + _isHandlingSelf = _charaDataManager.HandledCharaData.Any(c => c.Value.IsSelf); + if (_isHandlingSelf) _openMcdOnlineOnNextRun = false; + + using (var gposeTogetherTabItem = ImRaii.TabItem("GPose Together")) + { + if (gposeTogetherTabItem) + { + smallUi = true; + + DrawGposeTogether(); + } + } + + using (var applicationTabItem = ImRaii.TabItem("Data Application", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None)) + { + if (applicationTabItem) + { + smallUi = true; + using var appTabs = ImRaii.TabBar("TabsApplicationLevel"); + + using (ImRaii.Disabled(!_uiSharedService.IsInGpose)) + { + using (var gposeTabItem = ImRaii.TabItem("GPose Actors")) + { + if (gposeTabItem) + { + using var id = ImRaii.PushId("gposeControls"); + DrawGposeControls(); + } + } + } + if (!_uiSharedService.IsInGpose) + UiSharedService.AttachToolTip("Only available in GPose"); + + using (var nearbyPosesTabItem = ImRaii.TabItem("Poses Nearby")) + { + if (nearbyPosesTabItem) + { + using var id = ImRaii.PushId("nearbyPoseControls"); + _charaDataNearbyManager.ComputeNearbyData = true; + + DrawNearbyPoses(); + } + else + { + _charaDataNearbyManager.ComputeNearbyData = false; + } + } + + using (var gposeTabItem = ImRaii.TabItem("Apply Data", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None)) + { + if (gposeTabItem) + { + smallUi |= true; + using var id = ImRaii.PushId("applyData"); + DrawDataApplication(); + } + } + } + else + { + _charaDataNearbyManager.ComputeNearbyData = false; + } + } + + using (ImRaii.Disabled(_isHandlingSelf)) + { + ImGuiTabItemFlags flagsTopLevel = ImGuiTabItemFlags.None; + if (_openMcdOnlineOnNextRun) + { + flagsTopLevel = ImGuiTabItemFlags.SetSelected; + _openMcdOnlineOnNextRun = false; + } + + using (var creationTabItem = ImRaii.TabItem("Data Creation", flagsTopLevel)) + { + if (creationTabItem) + { + using var creationTabs = ImRaii.TabBar("TabsCreationLevel"); + + ImGuiTabItemFlags flags = ImGuiTabItemFlags.None; + if (_openMcdOnlineOnNextRun) + { + flags = ImGuiTabItemFlags.SetSelected; + _openMcdOnlineOnNextRun = false; + } + using (var mcdOnlineTabItem = ImRaii.TabItem("Online Data", flags)) + { + if (mcdOnlineTabItem) + { + using var id = ImRaii.PushId("mcdOnline"); + DrawMcdOnline(); + } + } + + using (var mcdfTabItem = ImRaii.TabItem("MCDF Export")) + { + if (mcdfTabItem) + { + using var id = ImRaii.PushId("mcdfExport"); + DrawMcdfExport(); + } + } + } + } + } + if (_isHandlingSelf) + { + UiSharedService.AttachToolTip("Cannot use creation tools while having Character Data applied to self."); + } + + using (var settingsTabItem = ImRaii.TabItem("Settings")) + { + if (settingsTabItem) + { + using var id = ImRaii.PushId("settings"); + DrawSettings(); + } + } + + + SetWindowSizeConstraints(smallUi); + } + + private void DrawAddOrRemoveFavorite(CharaDataFullDto dto) + { + DrawFavorite(dto.Uploader.UID + ":" + dto.Id); + } + + private void DrawAddOrRemoveFavorite(CharaDataMetaInfoExtendedDto? dto) + { + if (dto == null) return; + DrawFavorite(dto.FullId); + } + + private void DrawFavorite(string id) + { + bool isFavorite = _configService.Current.FavoriteCodes.TryGetValue(id, out var favorite); + if (_configService.Current.FavoriteCodes.ContainsKey(id)) + { + _uiSharedService.IconText(FontAwesomeIcon.Star, ImGuiColors.ParsedGold); + UiSharedService.AttachToolTip($"Custom Description: {favorite?.CustomDescription ?? string.Empty}" + UiSharedService.TooltipSeparator + + "Click to remove from Favorites"); + } + else + { + _uiSharedService.IconText(FontAwesomeIcon.Star, ImGuiColors.DalamudGrey); + UiSharedService.AttachToolTip("Click to add to Favorites"); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + if (isFavorite) _configService.Current.FavoriteCodes.Remove(id); + else _configService.Current.FavoriteCodes[id] = new(); + _configService.Save(); + } + } + + private void DrawGposeControls() + { + _uiSharedService.BigText("GPose Actors"); + ImGuiHelpers.ScaledDummy(5); + using var indent = ImRaii.PushIndent(10f); + + foreach (var actor in _dalamudUtilService.GetGposeCharactersFromObjectTable()) + { + if (actor == null) continue; + using var actorId = ImRaii.PushId(actor.Name.TextValue); + UiSharedService.DrawGrouped(() => + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Crosshairs)) + { + unsafe + { + _dalamudUtilService.GposeTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actor.Address; + } + } + ImGui.SameLine(); + UiSharedService.AttachToolTip($"Target the GPose Character {CharaName(actor.Name.TextValue)}"); + ImGui.AlignTextToFramePadding(); + var pos = ImGui.GetCursorPosX(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen, actor.Address == (_dalamudUtilService.GetGposeTargetGameObjectAsync().GetAwaiter().GetResult()?.Address ?? nint.Zero))) + { + ImGui.TextUnformatted(CharaName(actor.Name.TextValue)); + } + ImGui.SameLine(250); + var handled = _charaDataManager.HandledCharaData.GetValueOrDefault(actor.Name.TextValue); + using (ImRaii.Disabled(handled == null)) + { + _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); + var id = string.IsNullOrEmpty(handled?.MetaInfo.Uploader.UID) ? handled?.MetaInfo.Id : handled.MetaInfo.FullId; + UiSharedService.AttachToolTip($"Applied Data: {id ?? "No data applied"}"); + + ImGui.SameLine(); + // maybe do this better, check with brio for handled charas or sth + using (ImRaii.Disabled(!actor.Name.TextValue.StartsWith("Brio ", StringComparison.Ordinal))) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Trash)) + { + _charaDataManager.RemoveChara(actor.Name.TextValue); + } + UiSharedService.AttachToolTip($"Remove character {CharaName(actor.Name.TextValue)}"); + } + ImGui.SameLine(); + if (_uiSharedService.IconButton(FontAwesomeIcon.Undo)) + { + _charaDataManager.RevertChara(handled); + } + UiSharedService.AttachToolTip($"Revert applied data from {CharaName(actor.Name.TextValue)}"); + ImGui.SetCursorPosX(pos); + DrawPoseData(handled?.MetaInfo, actor.Name.TextValue, true); + } + }); + + ImGuiHelpers.ScaledDummy(2); + } + } + + private void DrawDataApplication() + { + _uiSharedService.BigText("Apply Character Appearance"); + + ImGuiHelpers.ScaledDummy(5); + + if (_uiSharedService.IsInGpose) + { + ImGui.TextUnformatted("GPose Target"); + ImGui.SameLine(200); + UiSharedService.ColorText(CharaName(_gposeTarget), UiSharedService.GetBoolColor(_hasValidGposeTarget)); + } + + if (!_hasValidGposeTarget) + { + ImGuiHelpers.ScaledDummy(3); + UiSharedService.DrawGroupedCenteredColorText("Applying data is only available in GPose with a valid selected GPose target.", ImGuiColors.DalamudYellow, 350); + } + + ImGuiHelpers.ScaledDummy(10); + + using var tabs = ImRaii.TabBar("Tabs"); + + using (var byFavoriteTabItem = ImRaii.TabItem("Favorites")) + { + if (byFavoriteTabItem) + { + using var id = ImRaii.PushId("byFavorite"); + + ImGuiHelpers.ScaledDummy(5); + + var max = ImGui.GetWindowContentRegionMax(); + UiSharedService.DrawTree("Filters", () => + { + var maxIndent = ImGui.GetWindowContentRegionMax(); + ImGui.SetNextItemWidth(maxIndent.X - ImGui.GetCursorPosX()); + ImGui.InputTextWithHint("##ownFilter", "Code/Owner Filter", ref _filterCodeNote, 100); + ImGui.SetNextItemWidth(maxIndent.X - ImGui.GetCursorPosX()); + ImGui.InputTextWithHint("##descFilter", "Custom Description Filter", ref _filterDescription, 100); + ImGui.Checkbox("Only show entries with pose data", ref _filterPoseOnly); + ImGui.Checkbox("Only show entries with world data", ref _filterWorldOnly); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Reset Filter")) + { + _filterCodeNote = string.Empty; + _filterDescription = string.Empty; + _filterPoseOnly = false; + _filterWorldOnly = false; + } + }); + + ImGuiHelpers.ScaledDummy(5); + ImGui.Separator(); + using var scrollableChild = ImRaii.Child("favorite"); + ImGuiHelpers.ScaledDummy(5); + using var totalIndent = ImRaii.PushIndent(5f); + var cursorPos = ImGui.GetCursorPos(); + max = ImGui.GetWindowContentRegionMax(); + foreach (var favorite in _filteredFavorites.OrderByDescending(k => k.Value.Favorite.LastDownloaded)) + { + UiSharedService.DrawGrouped(() => + { + using var tableid = ImRaii.PushId(favorite.Key); + ImGui.AlignTextToFramePadding(); + DrawFavorite(favorite.Key); + using var innerIndent = ImRaii.PushIndent(25f); + ImGui.SameLine(); + var xPos = ImGui.GetCursorPosX(); + var maxPos = (max.X - cursorPos.X); + + bool metaInfoDownloaded = favorite.Value.DownloadedMetaInfo; + var metaInfo = favorite.Value.MetaInfo; + + ImGui.AlignTextToFramePadding(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey, !metaInfoDownloaded)) + using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.GetBoolColor(metaInfo != null), metaInfoDownloaded)) + ImGui.TextUnformatted(favorite.Key); + + var iconSize = _uiSharedService.GetIconData(FontAwesomeIcon.Check); + var refreshButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.ArrowsSpin); + var applyButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.ArrowRight); + var addButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus); + var offsetFromRight = maxPos - (iconSize.X + refreshButtonSize.X + applyButtonSize.X + addButtonSize.X + (ImGui.GetStyle().ItemSpacing.X * 3.5f)); + + ImGui.SameLine(); + ImGui.SetCursorPosX(offsetFromRight); + if (metaInfoDownloaded) + { + _uiSharedService.BooleanToColoredIcon(metaInfo != null, false); + if (metaInfo != null) + { + UiSharedService.AttachToolTip("Metainfo present" + UiSharedService.TooltipSeparator + + $"Last Updated: {metaInfo!.UpdatedDate}" + Environment.NewLine + + $"Description: {metaInfo!.Description}" + Environment.NewLine + + $"Poses: {metaInfo!.PoseData.Count}"); + } + else + { + UiSharedService.AttachToolTip("Metainfo could not be downloaded." + UiSharedService.TooltipSeparator + + "The data associated with the code is either not present on the server anymore or you have no access to it"); + } + } + else + { + _uiSharedService.IconText(FontAwesomeIcon.QuestionCircle, ImGuiColors.DalamudGrey); + UiSharedService.AttachToolTip("Unknown accessibility state. Click the button on the right to refresh."); + } + + ImGui.SameLine(); + bool isInTimeout = _charaDataManager.IsInTimeout(favorite.Key); + using (ImRaii.Disabled(isInTimeout)) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.ArrowsSpin)) + { + _charaDataManager.DownloadMetaInfo(favorite.Key, false); + UpdateFilteredItems(); + } + } + UiSharedService.AttachToolTip(isInTimeout ? "Timeout for refreshing active, please wait before refreshing again." + : "Refresh data for this entry from the Server."); + + ImGui.SameLine(); + GposeMetaInfoAction((meta) => + { + if (_uiSharedService.IconButton(FontAwesomeIcon.ArrowRight)) + { + _ = _charaDataManager.ApplyCharaDataToGposeTarget(metaInfo!); + } + }, "Apply Character Data to GPose Target", metaInfo, _hasValidGposeTarget, false); + ImGui.SameLine(); + GposeMetaInfoAction((meta) => + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) + { + _ = _charaDataManager.SpawnAndApplyData(meta!); + } + }, "Spawn Actor with Brio and apply Character Data", metaInfo, _hasValidGposeTarget, true); + + string uidText = string.Empty; + var uid = favorite.Key.Split(":")[0]; + if (metaInfo != null) + { + uidText = metaInfo.Uploader.AliasOrUID; + } + else + { + uidText = uid; + } + + var note = _serverConfigurationManager.GetNoteForUid(uid); + if (note != null) + { + uidText = $"{note} ({uidText})"; + } + ImGui.TextUnformatted(uidText); + + ImGui.TextUnformatted("Last Use: "); + ImGui.SameLine(); + ImGui.TextUnformatted(favorite.Value.Favorite.LastDownloaded == DateTime.MaxValue ? "Never" : favorite.Value.Favorite.LastDownloaded.ToString()); + + var desc = favorite.Value.Favorite.CustomDescription; + ImGui.SetNextItemWidth(maxPos - xPos); + if (ImGui.InputTextWithHint("##desc", "Custom Description for Favorite", ref desc, 100)) + { + favorite.Value.Favorite.CustomDescription = desc; + _configService.Save(); + } + + DrawPoseData(metaInfo, _gposeTarget, _hasValidGposeTarget); + }); + + ImGuiHelpers.ScaledDummy(5); + } + + if (_configService.Current.FavoriteCodes.Count == 0) + { + UiSharedService.ColorTextWrapped("You have no favorites added. Add Favorites through the other tabs before you can use this tab.", ImGuiColors.DalamudYellow); + } + } + } + + using (var byCodeTabItem = ImRaii.TabItem("Code")) + { + using var id = ImRaii.PushId("byCodeTab"); + if (byCodeTabItem) + { + using var child = ImRaii.Child("sharedWithYouByCode", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize); + DrawHelpFoldout("You can apply character data you have a code for in this tab. Provide the code in it's given format \"OwnerUID:DataId\" into the field below and click on " + + "\"Get Info from Code\". This will provide you basic information about the data behind the code. Afterwards select an actor in GPose and press on \"Download and apply to \"." + Environment.NewLine + Environment.NewLine + + "Description: as set by the owner of the code to give you more or additional information of what this code may contain." + Environment.NewLine + + "Last Update: the date and time the owner of the code has last updated the data." + Environment.NewLine + + "Is Downloadable: whether or not the code is downloadable and applicable. If the code is not downloadable, contact the owner so they can attempt to fix it." + Environment.NewLine + Environment.NewLine + + "To download a code the code requires correct access permissions to be set by the owner. If getting info from the code fails, contact the owner to make sure they set their Access Permissions for the code correctly."); + + ImGuiHelpers.ScaledDummy(5); + ImGui.InputTextWithHint("##importCode", "Enter Data Code", ref _importCode, 100); + using (ImRaii.Disabled(string.IsNullOrEmpty(_importCode))) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Get Info from Code")) + { + _charaDataManager.DownloadMetaInfo(_importCode); + } + } + GposeMetaInfoAction((meta) => + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, $"Download and Apply")) + { + _ = _charaDataManager.ApplyCharaDataToGposeTarget(meta!); + } + }, "Apply this Character Data to the current GPose actor", _charaDataManager.LastDownloadedMetaInfo, _hasValidGposeTarget, false); + ImGui.SameLine(); + GposeMetaInfoAction((meta) => + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, $"Download and Spawn")) + { + _ = _charaDataManager.SpawnAndApplyData(meta!); + } + }, "Spawn a new Brio actor and apply this Character Data", _charaDataManager.LastDownloadedMetaInfo, _hasValidGposeTarget, true); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + DrawAddOrRemoveFavorite(_charaDataManager.LastDownloadedMetaInfo); + + ImGui.NewLine(); + if (!_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) + { + UiSharedService.ColorTextWrapped("Downloading meta info. Please wait.", ImGuiColors.DalamudYellow); + } + if ((_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) && !_charaDataManager.DownloadMetaInfoTask.Result.Success) + { + UiSharedService.ColorTextWrapped(_charaDataManager.DownloadMetaInfoTask.Result.Result, ImGuiColors.DalamudRed); + } + + using (ImRaii.Disabled(_charaDataManager.LastDownloadedMetaInfo == null)) + { + ImGuiHelpers.ScaledDummy(5); + var metaInfo = _charaDataManager.LastDownloadedMetaInfo; + ImGui.TextUnformatted("Description"); + ImGui.SameLine(150); + UiSharedService.TextWrapped(string.IsNullOrEmpty(metaInfo?.Description) ? "-" : metaInfo.Description); + ImGui.TextUnformatted("Last Update"); + ImGui.SameLine(150); + ImGui.TextUnformatted(metaInfo?.UpdatedDate.ToLocalTime().ToString() ?? "-"); + ImGui.TextUnformatted("Is Downloadable"); + ImGui.SameLine(150); + _uiSharedService.BooleanToColoredIcon(metaInfo?.CanBeDownloaded ?? false, inline: false); + ImGui.TextUnformatted("Poses"); + ImGui.SameLine(150); + if (metaInfo?.HasPoses ?? false) + DrawPoseData(metaInfo, _gposeTarget, _hasValidGposeTarget); + else + _uiSharedService.BooleanToColoredIcon(false, false); + } + } + } + + using (var yourOwnTabItem = ImRaii.TabItem("Your Own")) + { + using var id = ImRaii.PushId("yourOwnTab"); + if (yourOwnTabItem) + { + DrawHelpFoldout("You can apply character data you created yourself in this tab. If the list is not populated press on \"Download your Character Data\"." + Environment.NewLine + Environment.NewLine + + "To create new and edit your existing character data use the \"Online Data\" tab."); + + ImGuiHelpers.ScaledDummy(5); + + using (ImRaii.Disabled(_charaDataManager.GetAllDataTask != null + || (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted))) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleDown, "Download your Character Data")) + { + _ = _charaDataManager.GetAllData(_disposalCts.Token); + } + } + if (_charaDataManager.DataGetTimeoutTask != null && !_charaDataManager.DataGetTimeoutTask.IsCompleted) + { + UiSharedService.AttachToolTip("You can only refresh all character data from server every minute. Please wait."); + } + + ImGuiHelpers.ScaledDummy(5); + ImGui.Separator(); + + using var child = ImRaii.Child("ownDataChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize); + using var indent = ImRaii.PushIndent(10f); + foreach (var data in _charaDataManager.OwnCharaData.Values) + { + var hasMetaInfo = _charaDataManager.TryGetMetaInfo(data.FullId, out var metaInfo); + if (!hasMetaInfo) continue; + DrawMetaInfoData(_gposeTarget, _hasValidGposeTarget, metaInfo!, true); + } + + ImGuiHelpers.ScaledDummy(5); + } + } + + using (var sharedWithYouTabItem = ImRaii.TabItem("Shared With You", _openDataApplicationShared ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None)) + { + using var id = ImRaii.PushId("sharedWithYouTab"); + if (sharedWithYouTabItem) + { + DrawHelpFoldout("You can apply character data shared with you implicitly in this tab. Shared Character Data are Character Data entries that have \"Sharing\" set to \"Shared\" and you have access through those by meeting the access restrictions, " + + "i.e. you were specified by your UID to gain access or are paired with the other user according to the Access Restrictions setting." + Environment.NewLine + Environment.NewLine + + "Filter if needed to find a specific entry, then just press on \"Apply to \" and it will download and apply the Character Data to the currently targeted GPose actor." + Environment.NewLine + Environment.NewLine + + "Note: Shared Data of Pairs you have paused will not be shown here."); + + ImGuiHelpers.ScaledDummy(5); + + DrawUpdateSharedDataButton(); + + int activeFilters = 0; + if (!string.IsNullOrEmpty(_sharedWithYouOwnerFilter)) activeFilters++; + if (!string.IsNullOrEmpty(_sharedWithYouDescriptionFilter)) activeFilters++; + if (_sharedWithYouDownloadableFilter) activeFilters++; + string filtersText = activeFilters == 0 ? "Filters" : $"Filters ({activeFilters} active)"; + UiSharedService.DrawTree($"{filtersText}##filters", () => + { + var filterWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + ImGui.SetNextItemWidth(filterWidth); + if (ImGui.InputTextWithHint("##filter", "Filter by UID/Note", ref _sharedWithYouOwnerFilter, 30)) + { + UpdateFilteredItems(); + } + ImGui.SetNextItemWidth(filterWidth); + if (ImGui.InputTextWithHint("##filterDesc", "Filter by Description", ref _sharedWithYouDescriptionFilter, 50)) + { + UpdateFilteredItems(); + } + if (ImGui.Checkbox("Only show downloadable", ref _sharedWithYouDownloadableFilter)) + { + UpdateFilteredItems(); + } + }); + + if (_filteredDict == null && _charaDataManager.GetSharedWithYouTask == null) + { + _filteredDict = _charaDataManager.SharedWithYouData + .ToDictionary(k => + { + var note = _serverConfigurationManager.GetNoteForUid(k.Key.UID); + if (note == null) return k.Key.AliasOrUID; + return $"{note} ({k.Key.AliasOrUID})"; + }, k => k.Value, StringComparer.OrdinalIgnoreCase) + .Where(k => string.IsNullOrEmpty(_sharedWithYouOwnerFilter) || k.Key.Contains(_sharedWithYouOwnerFilter)) + .OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase).ToDictionary(); + } + + ImGuiHelpers.ScaledDummy(5); + ImGui.Separator(); + using var child = ImRaii.Child("sharedWithYouChild", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize); + + ImGuiHelpers.ScaledDummy(5); + foreach (var entry in _filteredDict ?? []) + { + bool isFilteredAndHasToBeOpened = entry.Key.Contains(_sharedWithYouOwnerFilter) && _openDataApplicationShared; + if (isFilteredAndHasToBeOpened) + ImGui.SetNextItemOpen(isFilteredAndHasToBeOpened); + UiSharedService.DrawTree($"{entry.Key} - [{entry.Value.Count} Character Data Sets]##{entry.Key}", () => + { + foreach (var data in entry.Value) + { + DrawMetaInfoData(_gposeTarget, _hasValidGposeTarget, data); + } + ImGuiHelpers.ScaledDummy(5); + }); + if (isFilteredAndHasToBeOpened) + _openDataApplicationShared = false; + } + } + } + + using (var mcdfTabItem = ImRaii.TabItem("From MCDF")) + { + using var id = ImRaii.PushId("applyMcdfTab"); + if (mcdfTabItem) + { + using var child = ImRaii.Child("applyMcdf", new(0, 0), false, ImGuiWindowFlags.AlwaysAutoResize); + DrawHelpFoldout("You can apply character data shared with you using a MCDF file in this tab." + Environment.NewLine + Environment.NewLine + + "Load the MCDF first via the \"Load MCDF\" button which will give you the basic description that the owner has set during export." + Environment.NewLine + + "You can then apply it to any handled GPose actor." + Environment.NewLine + Environment.NewLine + + "MCDF to share with others can be generated using the \"MCDF Export\" tab at the top."); + + ImGuiHelpers.ScaledDummy(5); + + if (_charaDataManager.LoadedMcdfHeader == null || _charaDataManager.LoadedMcdfHeader.IsCompleted) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.FolderOpen, "Load MCDF")) + { + _fileDialogManager.OpenFileDialog("Pick MCDF file", ".mcdf", (success, paths) => + { + if (!success) return; + if (paths.FirstOrDefault() is not string path) return; + + _configService.Current.LastSavedCharaDataLocation = Path.GetDirectoryName(path) ?? string.Empty; + _configService.Save(); + + _charaDataManager.LoadMcdf(path); + }, 1, Directory.Exists(_configService.Current.LastSavedCharaDataLocation) ? _configService.Current.LastSavedCharaDataLocation : null); + } + UiSharedService.AttachToolTip("Load MCDF Metadata into memory"); + if ((_charaDataManager.LoadedMcdfHeader?.IsCompleted ?? false)) + { + ImGui.TextUnformatted("Loaded file"); + ImGui.SameLine(200); + UiSharedService.TextWrapped(_charaDataManager.LoadedMcdfHeader.Result.LoadedFile.FilePath); + ImGui.Text("Description"); + ImGui.SameLine(200); + UiSharedService.TextWrapped(_charaDataManager.LoadedMcdfHeader.Result.LoadedFile.CharaFileData.Description); + + ImGuiHelpers.ScaledDummy(5); + + using (ImRaii.Disabled(!_hasValidGposeTarget)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply")) + { + _ = _charaDataManager.McdfApplyToGposeTarget(); + } + UiSharedService.AttachToolTip($"Apply to {_gposeTarget}"); + ImGui.SameLine(); + using (ImRaii.Disabled(!_charaDataManager.BrioAvailable)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Spawn Actor and Apply")) + { + _charaDataManager.McdfSpawnApplyToGposeTarget(); + } + } + } + } + if ((_charaDataManager.LoadedMcdfHeader?.IsFaulted ?? false) || (_charaDataManager.McdfApplicationTask?.IsFaulted ?? false)) + { + UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.", + ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " + + "If you received it from someone else have them do the same.", ImGuiColors.DalamudYellow); + } + } + else + { + UiSharedService.ColorTextWrapped("Loading Character...", ImGuiColors.DalamudYellow); + } + } + } + } + + private void DrawMcdfExport() + { + _uiSharedService.BigText("MCDF File Export"); + + DrawHelpFoldout("This feature allows you to pack your character into a MCDF file and manually send it to other people. MCDF files be imported during GPose. " + + "Be aware that the possibility exists that people write unofficial custom exporters to extract the containing data."); + + ImGuiHelpers.ScaledDummy(5); + + ImGui.Checkbox("##readExport", ref _readExport); + ImGui.SameLine(); + UiSharedService.TextWrapped("I understand that by exporting my character data into a file and sending it to other people I am giving away my current character appearance irrevocably. People I am sharing my data with have the ability to share it with other people without limitations."); + + if (_readExport) + { + ImGui.Indent(); + + ImGui.InputTextWithHint("Export Descriptor", "This description will be shown on loading the data", ref _exportDescription, 255); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Export Character as MCDF")) + { + string defaultFileName = string.IsNullOrEmpty(_exportDescription) + ? "export.mcdf" + : string.Join('_', $"{_exportDescription}.mcdf".Split(Path.GetInvalidFileNameChars())); + _uiSharedService.FileDialogManager.SaveFileDialog("Export Character to file", ".mcdf", defaultFileName, ".mcdf", (success, path) => + { + if (!success) return; + + _configService.Current.LastSavedCharaDataLocation = Path.GetDirectoryName(path) ?? string.Empty; + _configService.Save(); + + _charaDataManager.SaveMareCharaFile(_exportDescription, path); + _exportDescription = string.Empty; + }, Directory.Exists(_configService.Current.LastSavedCharaDataLocation) ? _configService.Current.LastSavedCharaDataLocation : null); + } + UiSharedService.ColorTextWrapped("Note: For best results make sure you have everything you want to be shared as well as the correct character appearance" + + " equipped and redraw your character before exporting.", ImGuiColors.DalamudYellow); + + ImGui.Unindent(); + } + } + + private void DrawMetaInfoData(string selectedGposeActor, bool hasValidGposeTarget, CharaDataMetaInfoExtendedDto data, bool canOpen = false) + { + ImGuiHelpers.ScaledDummy(5); + using var entryId = ImRaii.PushId(data.FullId); + + var startPos = ImGui.GetCursorPosX(); + var maxPos = ImGui.GetWindowContentRegionMax().X; + var availableWidth = maxPos - startPos; + UiSharedService.DrawGrouped(() => + { + ImGui.AlignTextToFramePadding(); + DrawAddOrRemoveFavorite(data); + + ImGui.SameLine(); + var favPos = ImGui.GetCursorPosX(); + ImGui.AlignTextToFramePadding(); + UiSharedService.ColorText(data.FullId, UiSharedService.GetBoolColor(data.CanBeDownloaded)); + if (!data.CanBeDownloaded) + { + UiSharedService.AttachToolTip("This data is incomplete on the server and cannot be downloaded. Contact the owner so they can fix it. If you are the owner, review the data in the Online Data tab."); + } + + var offsetFromRight = availableWidth - _uiSharedService.GetIconData(FontAwesomeIcon.Calendar).X - _uiSharedService.GetIconButtonSize(FontAwesomeIcon.ArrowRight).X + - _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X - ImGui.GetStyle().ItemSpacing.X * 2; + + ImGui.SameLine(); + ImGui.SetCursorPosX(offsetFromRight); + _uiSharedService.IconText(FontAwesomeIcon.Calendar); + UiSharedService.AttachToolTip($"Last Update: {data.UpdatedDate}"); + + ImGui.SameLine(); + GposeMetaInfoAction((meta) => + { + if (_uiSharedService.IconButton(FontAwesomeIcon.ArrowRight)) + { + _ = _charaDataManager.ApplyCharaDataToGposeTarget(meta!); + } + }, $"Apply Character data to {CharaName(selectedGposeActor)}", data, hasValidGposeTarget, false); + ImGui.SameLine(); + GposeMetaInfoAction((meta) => + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) + { + _ = _charaDataManager.SpawnAndApplyData(meta!); + } + }, "Spawn and Apply Character data", data, hasValidGposeTarget, true); + + using var indent = ImRaii.PushIndent(favPos - startPos); + + if (canOpen) + { + using (ImRaii.Disabled(_isHandlingSelf)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Edit, "Open in Online Data Editor")) + { + SelectedDtoId = data.Id; + _openMcdOnlineOnNextRun = true; + } + } + if (_isHandlingSelf) + { + UiSharedService.AttachToolTip("Cannot use Online Data while having Character Data applied to self."); + } + } + + if (string.IsNullOrEmpty(data.Description)) + { + UiSharedService.ColorTextWrapped("No description set", ImGuiColors.DalamudGrey, availableWidth); + } + else + { + UiSharedService.TextWrapped(data.Description, availableWidth); + } + + DrawPoseData(data, selectedGposeActor, hasValidGposeTarget); + }); + } + + + private void DrawPoseData(CharaDataMetaInfoExtendedDto? metaInfo, string actor, bool hasValidGposeTarget) + { + if (metaInfo == null || !metaInfo.HasPoses) return; + + bool isInGpose = _uiSharedService.IsInGpose; + var start = ImGui.GetCursorPosX(); + foreach (var item in metaInfo.PoseExtended) + { + if (!item.HasPoseData) continue; + + float DrawIcon(float s) + { + ImGui.SetCursorPosX(s); + var posX = ImGui.GetCursorPosX(); + _uiSharedService.IconText(item.HasWorldData ? FontAwesomeIcon.Circle : FontAwesomeIcon.Running); + if (item.HasWorldData) + { + ImGui.SameLine(); + ImGui.SetCursorPosX(posX); + using var col = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.WindowBg)); + _uiSharedService.IconText(FontAwesomeIcon.Running); + ImGui.SameLine(); + ImGui.SetCursorPosX(posX); + _uiSharedService.IconText(FontAwesomeIcon.Running); + } + ImGui.SameLine(); + return ImGui.GetCursorPosX(); + } + + string tooltip = string.IsNullOrEmpty(item.Description) ? "No description set" : "Pose Description: " + item.Description; + if (!isInGpose) + { + start = DrawIcon(start); + UiSharedService.AttachToolTip(tooltip + UiSharedService.TooltipSeparator + (item.HasWorldData ? GetWorldDataTooltipText(item) + UiSharedService.TooltipSeparator + "Click to show on Map" : string.Empty)); + if (item.HasWorldData && ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + _dalamudUtilService.SetMarkerAndOpenMap(item.Position, item.Map); + } + } + else + { + tooltip += UiSharedService.TooltipSeparator + $"Left Click: Apply this pose to {CharaName(actor)}"; + if (item.HasWorldData) tooltip += Environment.NewLine + $"CTRL+Right Click: Apply world position to {CharaName(actor)}." + + UiSharedService.TooltipSeparator + "!!! CAUTION: Applying world position will likely yeet this actor into nirvana. Use at your own risk !!!"; + GposePoseAction(() => + { + start = DrawIcon(start); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + _ = _charaDataManager.ApplyPoseData(item, actor); + } + if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && UiSharedService.CtrlPressed()) + { + _ = _charaDataManager.ApplyWorldDataToTarget(item, actor); + } + }, tooltip, hasValidGposeTarget); + ImGui.SameLine(); + } + } + if (metaInfo.PoseExtended.Any()) ImGui.NewLine(); + } + + private void DrawSettings() + { + ImGuiHelpers.ScaledDummy(5); + _uiSharedService.BigText("Settings"); + ImGuiHelpers.ScaledDummy(5); + bool openInGpose = _configService.Current.OpenMareHubOnGposeStart; + if (ImGui.Checkbox("Open Character Data Hub when GPose loads", ref openInGpose)) + { + _configService.Current.OpenMareHubOnGposeStart = openInGpose; + _configService.Save(); + } + _uiSharedService.DrawHelpText("This will automatically open the import menu when loading into Gpose. If unchecked you can open the menu manually with /sync gpose"); + bool downloadDataOnConnection = _configService.Current.DownloadMcdDataOnConnection; + if (ImGui.Checkbox("Download Online Character Data on connecting", ref downloadDataOnConnection)) + { + _configService.Current.DownloadMcdDataOnConnection = downloadDataOnConnection; + _configService.Save(); + } + _uiSharedService.DrawHelpText("This will automatically download Online Character Data data (Your Own and Shared with You) once a connection is established to the server."); + + bool showHelpTexts = _configService.Current.ShowHelpTexts; + if (ImGui.Checkbox("Show \"What is this? (Explanation / Help)\" foldouts", ref showHelpTexts)) + { + _configService.Current.ShowHelpTexts = showHelpTexts; + _configService.Save(); + } + + ImGui.Checkbox("Abbreviate Chara Names", ref _abbreviateCharaName); + _uiSharedService.DrawHelpText("This setting will abbreviate displayed names. This setting is not persistent and will reset between restarts."); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Last Export Folder"); + ImGui.SameLine(300); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(string.IsNullOrEmpty(_configService.Current.LastSavedCharaDataLocation) ? "Not set" : _configService.Current.LastSavedCharaDataLocation); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Ban, "Clear Last Export Folder")) + { + _configService.Current.LastSavedCharaDataLocation = string.Empty; + _configService.Save(); + } + _uiSharedService.DrawHelpText("Use this if the Load or Save MCDF file dialog does not open"); + } + + private void DrawHelpFoldout(string text) + { + if (_configService.Current.ShowHelpTexts) + { + ImGuiHelpers.ScaledDummy(5); + UiSharedService.DrawTree("What is this? (Explanation / Help)", () => + { + UiSharedService.TextWrapped(text); + }); + } + } + + private void DisableDisabled(Action drawAction) + { + if (_disableUI) ImGui.EndDisabled(); + drawAction(); + if (_disableUI) ImGui.BeginDisabled(); + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs new file mode 100644 index 0000000..b52722b --- /dev/null +++ b/MareSynchronos/UI/CompactUI.cs @@ -0,0 +1,640 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.User; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI.Components; +using MareSynchronos.UI.Handlers; +using MareSynchronos.WebAPI; +using MareSynchronos.WebAPI.Files; +using MareSynchronos.WebAPI.Files.Models; +using MareSynchronos.WebAPI.SignalR.Utils; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Globalization; +using System.Numerics; +using System.Reflection; + +namespace MareSynchronos.UI; + +public class CompactUi : WindowMediatorSubscriberBase +{ + public float TransferPartHeight; + public float WindowContentWidth; + private readonly ApiController _apiController; + private readonly MareConfigService _configService; + private readonly ConcurrentDictionary> _currentDownloads = new(); + private readonly FileUploadManager _fileTransferManager; + private readonly GroupPanel _groupPanel; + private readonly PairGroupsUi _pairGroupsUi; + private readonly PairManager _pairManager; + private readonly SelectGroupForPairUi _selectGroupForPairUi; + private readonly SelectPairForGroupUi _selectPairsForGroupUi; + private readonly ServerConfigurationManager _serverManager; + private readonly Stopwatch _timeout = new(); + private readonly CharaDataManager _charaDataManager; + private readonly UidDisplayHandler _uidDisplayHandler; + private readonly UiSharedService _uiSharedService; + private bool _buttonState; + private string _characterOrCommentFilter = string.Empty; + private Pair? _lastAddedUser; + private string _lastAddedUserComment = string.Empty; + private Vector2 _lastPosition = Vector2.One; + private Vector2 _lastSize = Vector2.One; + private string _pairToAdd = string.Empty; + private int _secretKeyIdx = -1; + private bool _showModalForUserAddition; + private bool _showSyncShells; + private bool _wasOpen; + + public CompactUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService, + ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager, + PerformanceCollectorService performanceCollectorService) + : base(logger, mediator, "###SnowcloakSyncMainUI", performanceCollectorService) + { + _uiSharedService = uiShared; + _configService = configService; + _apiController = apiController; + _pairManager = pairManager; + _serverManager = serverManager; + _fileTransferManager = fileTransferManager; + _uidDisplayHandler = uidDisplayHandler; + _charaDataManager = charaDataManager; + var tagHandler = new TagHandler(_serverManager); + + _groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _configService, _serverManager, _charaDataManager); + _selectGroupForPairUi = new(tagHandler, uidDisplayHandler, _uiSharedService); + _selectPairsForGroupUi = new(tagHandler, uidDisplayHandler); + _pairGroupsUi = new(configService, tagHandler, uidDisplayHandler, apiController, _selectPairsForGroupUi, _uiSharedService); + +#if DEBUG + string dev = "Dev Build"; + var ver = Assembly.GetExecutingAssembly().GetName().Version!; + WindowName = $"Snowcloak Sync {dev} ({ver.Major}.{ver.Minor}.{ver.Build})###SnowcloakSyncMainUIDev"; + Toggle(); +#else + var ver = Assembly.GetExecutingAssembly().GetName().Version!; + WindowName = "Snowcloak Sync " + ver.Major + "." + ver.Minor + "." + ver.Build + "###SnowcloakSyncMainUI"; +#endif + Mediator.Subscribe(this, (_) => IsOpen = true); + Mediator.Subscribe(this, (_) => IsOpen = false); + Mediator.Subscribe(this, (_) => UiSharedService_GposeStart()); + Mediator.Subscribe(this, (_) => UiSharedService_GposeEnd()); + Mediator.Subscribe(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); + Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); + + Flags |= ImGuiWindowFlags.NoDocking; + + SizeConstraints = new WindowSizeConstraints() + { + MinimumSize = new Vector2(350, 400), + MaximumSize = new Vector2(350, 2000), + }; + } + + protected override void DrawInternal() + { + if (_serverManager.CurrentApiUrl.Equals(ApiController.SnowcloakServiceUri, StringComparison.Ordinal)) + UiSharedService.AccentColor = new(0.4275f, 0.6863f, 1f, 1f); + else + UiSharedService.AccentColor = ImGuiColors.ParsedGreen; + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y - 1f * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.Y); + WindowContentWidth = UiSharedService.GetWindowContentRegionWidth(); + if (!_apiController.IsCurrentVersion) + { + var ver = _apiController.CurrentClientVersion; + var unsupported = "UNSUPPORTED VERSION"; + using (_uiSharedService.UidFont.Push()) + { + var uidTextSize = ImGui.CalcTextSize(unsupported); + ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2); + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(ImGuiColors.DalamudRed, unsupported); + } + UiSharedService.ColorTextWrapped($"Your Snowcloak installation is out of date, the current version is {ver.Major}.{ver.Minor}.{ver.Build}. " + + $"It is highly recommended to keep Snowcloak up to date. Open /xlplugins and update the plugin.", ImGuiColors.DalamudRed); + } + + using (ImRaii.PushId("header")) DrawUIDHeader(); + ImGui.Separator(); + using (ImRaii.PushId("serverstatus")) DrawServerStatus(); + + if (_apiController.ServerState is ServerState.Connected) + { + var hasShownSyncShells = _showSyncShells; + + ImGui.PushFont(UiBuilder.IconFont); + if (!hasShownSyncShells) + { + ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]); + } + if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale))) + { + _showSyncShells = false; + } + if (!hasShownSyncShells) + { + ImGui.PopStyleColor(); + } + ImGui.PopFont(); + UiSharedService.AttachToolTip("Individual pairs"); + + ImGui.SameLine(); + + ImGui.PushFont(UiBuilder.IconFont); + if (hasShownSyncShells) + { + ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]); + } + if (ImGui.Button(FontAwesomeIcon.UserFriends.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale))) + { + _showSyncShells = true; + } + if (hasShownSyncShells) + { + ImGui.PopStyleColor(); + } + ImGui.PopFont(); + + UiSharedService.AttachToolTip("Syncshells"); + + ImGui.Separator(); + if (!hasShownSyncShells) + { + using (ImRaii.PushId("pairlist")) DrawPairList(); + } + else + { + using (ImRaii.PushId("syncshells")) _groupPanel.DrawSyncshells(); + } + ImGui.Separator(); + using (ImRaii.PushId("transfers")) DrawTransfers(); + TransferPartHeight = ImGui.GetCursorPosY() - TransferPartHeight; + using (ImRaii.PushId("group-user-popup")) _selectPairsForGroupUi.Draw(_pairManager.DirectPairs); + using (ImRaii.PushId("grouping-popup")) _selectGroupForPairUi.Draw(); + } + + if (_configService.Current.OpenPopupOnAdd && _pairManager.LastAddedUser != null) + { + _lastAddedUser = _pairManager.LastAddedUser; + _pairManager.LastAddedUser = null; + ImGui.OpenPopup("Set Notes for New User"); + _showModalForUserAddition = true; + _lastAddedUserComment = string.Empty; + } + + if (ImGui.BeginPopupModal("Set Notes for New User", ref _showModalForUserAddition, UiSharedService.PopupWindowFlags)) + { + if (_lastAddedUser == null) + { + _showModalForUserAddition = false; + } + else + { + UiSharedService.TextWrapped($"You have successfully added {_lastAddedUser.UserData.AliasOrUID}. Set a local note for the user in the field below:"); + ImGui.InputTextWithHint("##noteforuser", $"Note for {_lastAddedUser.UserData.AliasOrUID}", ref _lastAddedUserComment, 100); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Note")) + { + _serverManager.SetNoteForUid(_lastAddedUser.UserData.UID, _lastAddedUserComment); + _lastAddedUser = null; + _lastAddedUserComment = string.Empty; + _showModalForUserAddition = false; + } + } + UiSharedService.SetScaledWindowSize(275); + ImGui.EndPopup(); + } + + var pos = ImGui.GetWindowPos(); + var size = ImGui.GetWindowSize(); + if (_lastSize != size || _lastPosition != pos) + { + _lastSize = size; + _lastPosition = pos; + Mediator.Publish(new CompactUiChange(_lastSize, _lastPosition)); + } + } + + public override void OnClose() + { + _uidDisplayHandler.Clear(); + base.OnClose(); + } + + private void DrawAddCharacter() + { + ImGui.Dummy(new(10)); + var keys = _serverManager.CurrentServer!.SecretKeys; + if (keys.Any()) + { + if (_secretKeyIdx == -1) _secretKeyIdx = keys.First().Key; + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Plus, "Add current character with secret key")) + { + _serverManager.CurrentServer!.Authentications.Add(new MareConfiguration.Models.Authentication() + { + CharacterName = _uiSharedService.PlayerName, + WorldId = _uiSharedService.WorldId, + SecretKeyIdx = _secretKeyIdx + }); + + _serverManager.Save(); + + _ = _apiController.CreateConnections(); + } + + _uiSharedService.DrawCombo("Secret Key##addCharacterSecretKey", keys, (f) => f.Value.FriendlyName, (f) => _secretKeyIdx = f.Key); + } + else + { + UiSharedService.ColorTextWrapped("No secret keys are configured for the current server.", ImGuiColors.DalamudYellow); + } + } + + private void DrawAddPair() + { + var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus); + ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); + ImGui.InputTextWithHint("##otheruid", "Other players UID/Alias", ref _pairToAdd, 20); + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); + var canAdd = !_pairManager.DirectPairs.Any(p => string.Equals(p.UserData.UID, _pairToAdd, StringComparison.Ordinal) || string.Equals(p.UserData.Alias, _pairToAdd, StringComparison.Ordinal)); + using (ImRaii.Disabled(!canAdd)) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) + { + _ = _apiController.UserAddPair(new(new(_pairToAdd))); + _pairToAdd = string.Empty; + } + UiSharedService.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd)); + } + + ImGuiHelpers.ScaledDummy(2); + } + + private void DrawFilter() + { + var playButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Play); + + var users = GetFilteredUsers(); + var userCount = users.Count; + + var spacing = userCount > 0 + ? playButtonSize.X + ImGui.GetStyle().ItemSpacing.X + : 0; + + ImGui.SetNextItemWidth(WindowContentWidth - spacing); + ImGui.InputTextWithHint("##filter", "Filter for UID/notes", ref _characterOrCommentFilter, 255); + + if (userCount == 0) return; + + var pausedUsers = users.Where(u => u.UserPair!.OwnPermissions.IsPaused() && u.UserPair.OtherPermissions.IsPaired()).ToList(); + var resumedUsers = users.Where(u => !u.UserPair!.OwnPermissions.IsPaused() && u.UserPair.OtherPermissions.IsPaired()).ToList(); + + if (!pausedUsers.Any() && !resumedUsers.Any()) return; + ImGui.SameLine(); + + switch (_buttonState) + { + case true when !pausedUsers.Any(): + _buttonState = false; + break; + + case false when !resumedUsers.Any(): + _buttonState = true; + break; + + case true: + users = pausedUsers; + break; + + case false: + users = resumedUsers; + break; + } + + if (_timeout.ElapsedMilliseconds > 5000) + _timeout.Reset(); + + var button = _buttonState ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + + using (ImRaii.Disabled(_timeout.IsRunning)) + { + if (_uiSharedService.IconButton(button) && UiSharedService.CtrlPressed()) + { + foreach (var entry in users) + { + var perm = entry.UserPair!.OwnPermissions; + perm.SetPaused(!perm.IsPaused()); + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, perm)); + } + + _timeout.Start(); + _buttonState = !_buttonState; + } + if (!_timeout.IsRunning) + UiSharedService.AttachToolTip($"Hold Control to {(button == FontAwesomeIcon.Play ? "resume" : "pause")} pairing with {users.Count} out of {userCount} displayed users."); + else + UiSharedService.AttachToolTip($"Next execution is available at {(5000 - _timeout.ElapsedMilliseconds) / 1000} seconds"); + } + } + + private void DrawPairList() + { + using (ImRaii.PushId("addpair")) DrawAddPair(); + using (ImRaii.PushId("pairs")) DrawPairs(); + TransferPartHeight = ImGui.GetCursorPosY(); + using (ImRaii.PushId("filter")) DrawFilter(); + } + + private void DrawPairs() + { + var ySize = TransferPartHeight == 0 + ? 1 + : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - TransferPartHeight - ImGui.GetCursorPosY(); + var users = GetFilteredUsers().OrderBy(u => u.GetPairSortKey(), StringComparer.Ordinal); + + var onlineUsers = users.Where(u => u.UserPair!.OtherPermissions.IsPaired() && (u.IsOnline || u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Online" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager)).ToList(); + var visibleUsers = users.Where(u => u.IsVisible).Select(c => new DrawUserPair("Visible" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager)).ToList(); + var offlineUsers = users.Where(u => !u.UserPair!.OtherPermissions.IsPaired() || (!u.IsOnline && !u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Offline" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager)).ToList(); + + ImGui.BeginChild("list", new Vector2(WindowContentWidth, ySize), border: false); + + _pairGroupsUi.Draw(visibleUsers, onlineUsers, offlineUsers); + + ImGui.EndChild(); + } + + private void DrawServerStatus() + { + var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link); + var userCount = _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture); + var userSize = ImGui.CalcTextSize(userCount); + var textSize = ImGui.CalcTextSize("Users Online"); + string shardConnection = string.Equals(_apiController.ServerInfo.ShardName, "Main", StringComparison.OrdinalIgnoreCase) ? string.Empty : $"Shard: {_apiController.ServerInfo.ShardName}"; + var shardTextSize = ImGui.CalcTextSize(shardConnection); + var printShard = !string.IsNullOrEmpty(_apiController.ServerInfo.ShardName) && shardConnection != string.Empty; + + if (_apiController.ServerState is ServerState.Connected) + { + ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2); + if (!printShard) ImGui.AlignTextToFramePadding(); + ImGui.TextColored(ImGuiColors.ParsedGreen, userCount); + ImGui.SameLine(); + if (!printShard) ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Users Online"); + } + else + { + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(ImGuiColors.DalamudRed, "Not connected to any server"); + } + + if (printShard) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().ItemSpacing.Y); + ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - shardTextSize.X / 2); + ImGui.TextUnformatted(shardConnection); + } + + ImGui.SameLine(); + if (printShard) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2); + } + var color = UiSharedService.GetBoolColor(!_serverManager.CurrentServer!.FullPause); + var connectedIcon = !_serverManager.CurrentServer.FullPause ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink; + + if (_apiController.ServerState is ServerState.Connected) + { + ImGui.SetCursorPosX(0 + ImGui.GetStyle().ItemSpacing.X); + if (_uiSharedService.IconButton(FontAwesomeIcon.UserCircle)) + { + Mediator.Publish(new UiToggleMessage(typeof(EditProfileUi))); + } + UiSharedService.AttachToolTip("Edit your Profile"); + } + + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); + if (printShard) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2); + } + + if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting)) + { + ImGui.PushStyleColor(ImGuiCol.Text, color); + if (_uiSharedService.IconButton(connectedIcon)) + { + _serverManager.CurrentServer.FullPause = !_serverManager.CurrentServer.FullPause; + _serverManager.Save(); + _ = _apiController.CreateConnections(); + } + ImGui.PopStyleColor(); + UiSharedService.AttachToolTip(!_serverManager.CurrentServer.FullPause ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName); + } + } + + private void DrawTransfers() + { + var currentUploads = _fileTransferManager.CurrentUploads.ToList(); + + if (currentUploads.Any()) + { + ImGui.AlignTextToFramePadding(); + _uiSharedService.IconText(FontAwesomeIcon.Upload); + ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); + + 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); + + ImGui.TextUnformatted($"{doneUploads}/{totalUploads}"); + var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})"; + var textSize = ImGui.CalcTextSize(uploadText); + ImGui.SameLine(WindowContentWidth - textSize.X); + ImGui.TextUnformatted(uploadText); + } + + var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList(); + + if (currentDownloads.Any()) + { + ImGui.AlignTextToFramePadding(); + _uiSharedService.IconText(FontAwesomeIcon.Download); + ImGui.SameLine(35 * ImGuiHelpers.GlobalScale); + + var totalDownloads = currentDownloads.Sum(c => c.TotalFiles); + var doneDownloads = currentDownloads.Sum(c => c.TransferredFiles); + var totalDownloaded = currentDownloads.Sum(c => c.TransferredBytes); + var totalToDownload = currentDownloads.Sum(c => c.TotalBytes); + + ImGui.TextUnformatted($"{doneDownloads}/{totalDownloads}"); + var downloadText = + $"({UiSharedService.ByteToString(totalDownloaded)}/{UiSharedService.ByteToString(totalToDownload)})"; + var textSize = ImGui.CalcTextSize(downloadText); + ImGui.SameLine(WindowContentWidth - textSize.X); + ImGui.TextUnformatted(downloadText); + } + + var bottomButtonWidth = (WindowContentWidth - ImGui.GetStyle().ItemSpacing.X) / 2; + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Character Analysis", bottomButtonWidth)) + { + Mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); + } + + ImGui.SameLine(); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Running, "Character Data Hub", bottomButtonWidth)) + { + Mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi))); + } + + ImGui.SameLine(); + } + + private void DrawUIDHeader() + { + var uidText = GetUidText(); + var buttonSizeX = 0f; + Vector2 uidTextSize; + + using (_uiSharedService.UidFont.Push()) + { + uidTextSize = ImGui.CalcTextSize(uidText); + } + + var originalPos = ImGui.GetCursorPos(); + ImGui.SetWindowFontScale(1.5f); + var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog); + buttonSizeX -= buttonSize.X - ImGui.GetStyle().ItemSpacing.X * 2; + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); + ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2); + if (_uiSharedService.IconButton(FontAwesomeIcon.Cog)) + { + Mediator.Publish(new OpenSettingsUiMessage()); + } + UiSharedService.AttachToolTip("Open the Snowcloak Settings"); + + ImGui.SameLine(); //Important to draw the uidText consistently + ImGui.SetCursorPos(originalPos); + + if (_apiController.ServerState is ServerState.Connected) + { + buttonSizeX += _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Copy).X - ImGui.GetStyle().ItemSpacing.X * 2; + ImGui.SetCursorPosY(originalPos.Y + uidTextSize.Y / 2 - buttonSize.Y / 2); + if (_uiSharedService.IconButton(FontAwesomeIcon.Copy)) + { + ImGui.SetClipboardText(_apiController.DisplayName); + } + UiSharedService.AttachToolTip("Copy your UID to clipboard"); + ImGui.SameLine(); + } + ImGui.SetWindowFontScale(1f); + + ImGui.SetCursorPosY(originalPos.Y + buttonSize.Y / 2 - uidTextSize.Y / 2 - ImGui.GetStyle().ItemSpacing.Y / 2); + ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 + buttonSizeX - uidTextSize.X / 2); + using (_uiSharedService.UidFont.Push()) + ImGui.TextColored(GetUidColor(), uidText); + + if (_apiController.ServerState is not ServerState.Connected) + { + UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor()); + if (_apiController.ServerState is ServerState.NoSecretKey) + { + DrawAddCharacter(); + } + } + } + + private List GetFilteredUsers() + { + return _pairManager.DirectPairs.Where(p => + { + if (_characterOrCommentFilter.IsNullOrEmpty()) return true; + return p.UserData.AliasOrUID.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) || + (p.GetNote()?.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ?? false) || + (p.PlayerName?.Contains(_characterOrCommentFilter, StringComparison.OrdinalIgnoreCase) ?? false); + }).ToList(); + } + + private string GetServerError() + { + return _apiController.ServerState 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 sync server.", + ServerState.Disconnecting => "Disconnecting from the server", + ServerState.Unauthorized => "Server Response: " + _apiController.AuthFailureMessage, + ServerState.Offline => "Your selected 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. Use the button below or open the 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.", + _ => string.Empty + }; + } + + private Vector4 GetUidColor() + { + return _apiController.ServerState switch + { + ServerState.Connecting => ImGuiColors.DalamudYellow, + ServerState.Reconnecting => ImGuiColors.DalamudRed, + ServerState.Connected => UiSharedService.AccentColor, + ServerState.Disconnected => ImGuiColors.DalamudYellow, + ServerState.Disconnecting => ImGuiColors.DalamudYellow, + ServerState.Unauthorized => ImGuiColors.DalamudRed, + ServerState.VersionMisMatch => ImGuiColors.DalamudRed, + ServerState.Offline => ImGuiColors.DalamudRed, + ServerState.RateLimited => ImGuiColors.DalamudYellow, + ServerState.NoSecretKey => ImGuiColors.DalamudYellow, + ServerState.MultiChara => ImGuiColors.DalamudYellow, + _ => ImGuiColors.DalamudRed + }; + } + + private string GetUidText() + { + return _apiController.ServerState 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.Connected => _apiController.DisplayName, + _ => string.Empty + }; + } + + private void UiSharedService_GposeEnd() + { + IsOpen = _wasOpen; + } + + private void UiSharedService_GposeStart() + { + _wasOpen = IsOpen; + IsOpen = false; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/DrawGroupPair.cs b/MareSynchronos/UI/Components/DrawGroupPair.cs new file mode 100644 index 0000000..64db21f --- /dev/null +++ b/MareSynchronos/UI/Components/DrawGroupPair.cs @@ -0,0 +1,376 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.UI.Handlers; +using MareSynchronos.WebAPI; + +namespace MareSynchronos.UI.Components; + +public class DrawGroupPair : DrawPairBase +{ + protected readonly MareMediator _mediator; + private readonly GroupPairFullInfoDto _fullInfoDto; + private readonly GroupFullInfoDto _group; + private readonly CharaDataManager _charaDataManager; + + public DrawGroupPair(string id, Pair entry, ApiController apiController, + MareMediator mareMediator, GroupFullInfoDto group, GroupPairFullInfoDto fullInfoDto, + UidDisplayHandler handler, UiSharedService uiSharedService, CharaDataManager charaDataManager) + : base(id, entry, apiController, handler, uiSharedService) + { + _group = group; + _fullInfoDto = fullInfoDto; + _mediator = mareMediator; + _charaDataManager = charaDataManager; + } + + protected override void DrawLeftSide(float textPosY, float originalY) + { + var entryUID = _pair.UserData.AliasOrUID; + var entryIsMod = _fullInfoDto.GroupPairStatusInfo.IsModerator(); + var entryIsOwner = string.Equals(_pair.UserData.UID, _group.OwnerUID, StringComparison.Ordinal); + var entryIsPinned = _fullInfoDto.GroupPairStatusInfo.IsPinned(); + var presenceIcon = _pair.IsVisible ? FontAwesomeIcon.Eye : (_pair.IsOnline ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink); + var presenceColor = (_pair.IsOnline || _pair.IsVisible) ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + var presenceText = entryUID + " is offline"; + + ImGui.SetCursorPosY(textPosY); + if (_pair.IsPaused) + { + presenceIcon = FontAwesomeIcon.Question; + presenceColor = ImGuiColors.DalamudGrey; + presenceText = entryUID + " online status is unknown (paused)"; + + ImGui.PushFont(UiBuilder.IconFont); + UiSharedService.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow); + ImGui.PopFont(); + + UiSharedService.AttachToolTip("Pairing status with " + entryUID + " is paused"); + } + else + { + ImGui.PushFont(UiBuilder.IconFont); + UiSharedService.ColorText(FontAwesomeIcon.Check.ToIconString(), ImGuiColors.ParsedGreen); + ImGui.PopFont(); + + UiSharedService.AttachToolTip("You are paired with " + entryUID); + } + + if (_pair.IsOnline && !_pair.IsVisible) presenceText = entryUID + " is online"; + else if (_pair.IsOnline && _pair.IsVisible) presenceText = entryUID + " is visible: " + _pair.PlayerName + Environment.NewLine + "Click to target this player"; + + ImGui.SameLine(); + ImGui.SetCursorPosY(textPosY); + ImGui.PushFont(UiBuilder.IconFont); + UiSharedService.ColorText(presenceIcon.ToIconString(), presenceColor); + ImGui.PopFont(); + if (_pair.IsVisible) + { + if (ImGui.IsItemClicked()) + { + _mediator.Publish(new TargetPairMessage(_pair)); + } + if (_pair.LastAppliedDataBytes >= 0) + { + presenceText += UiSharedService.TooltipSeparator; + presenceText += ((!_pair.IsVisible) ? "(Last) " : string.Empty) + "Mods Info" + Environment.NewLine; + presenceText += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataBytes, true); + if (_pair.LastAppliedApproximateVRAMBytes >= 0) + { + presenceText += Environment.NewLine + "Approx. VRAM Usage: " + UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes, true); + } + if (_pair.LastAppliedDataTris >= 0) + { + presenceText += Environment.NewLine + "Triangle Count (excl. Vanilla): " + + (_pair.LastAppliedDataTris > 1000 ? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'") : _pair.LastAppliedDataTris); + } + } + } + UiSharedService.AttachToolTip(presenceText); + + if (entryIsOwner) + { + ImGui.SameLine(); + ImGui.SetCursorPosY(textPosY); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString()); + ImGui.PopFont(); + UiSharedService.AttachToolTip("User is owner of this Syncshell"); + } + else if (entryIsMod) + { + ImGui.SameLine(); + ImGui.SetCursorPosY(textPosY); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(FontAwesomeIcon.UserShield.ToIconString()); + ImGui.PopFont(); + UiSharedService.AttachToolTip("User is moderator of this Syncshell"); + } + else if (entryIsPinned) + { + ImGui.SameLine(); + ImGui.SetCursorPosY(textPosY); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(FontAwesomeIcon.Thumbtack.ToIconString()); + ImGui.PopFont(); + UiSharedService.AttachToolTip("User is pinned in this Syncshell"); + } + } + + protected override float DrawRightSide(float textPosY, float originalY) + { + var entryUID = _fullInfoDto.UserAliasOrUID; + var entryIsMod = _fullInfoDto.GroupPairStatusInfo.IsModerator(); + var entryIsOwner = string.Equals(_pair.UserData.UID, _group.OwnerUID, StringComparison.Ordinal); + var entryIsPinned = _fullInfoDto.GroupPairStatusInfo.IsPinned(); + var userIsOwner = string.Equals(_group.OwnerUID, _apiController.UID, StringComparison.OrdinalIgnoreCase); + var userIsModerator = _group.GroupUserInfo.IsModerator(); + + var soundsDisabled = _fullInfoDto.GroupUserPermissions.IsDisableSounds(); + var animDisabled = _fullInfoDto.GroupUserPermissions.IsDisableAnimations(); + var vfxDisabled = _fullInfoDto.GroupUserPermissions.IsDisableVFX(); + var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false); + var individualAnimDisabled = (_pair.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableAnimations() ?? false); + var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false); + + bool showShared = _charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData); + bool showInfo = (individualAnimDisabled || individualSoundsDisabled || animDisabled || soundsDisabled); + bool showPlus = _pair.UserPair == null; + bool showBars = (userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) || !_pair.IsPaused; + + var spacing = ImGui.GetStyle().ItemSpacing.X; + var permIcon = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) ? FontAwesomeIcon.ExclamationTriangle + : ((soundsDisabled || animDisabled || vfxDisabled) ? FontAwesomeIcon.InfoCircle : FontAwesomeIcon.None); + var runningIconWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X; + var infoIconWidth = UiSharedService.GetIconSize(permIcon).X; + var plusButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X; + var barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X; + + var pos = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() + spacing + - (showShared ? (runningIconWidth + spacing) : 0) + - (showInfo ? (infoIconWidth + spacing) : 0) + - (showPlus ? (plusButtonWidth + spacing) : 0) + - (showBars ? (barButtonWidth + spacing) : 0); + + ImGui.SameLine(pos); + + if (showShared) + { + _uiSharedService.IconText(FontAwesomeIcon.Running); + + UiSharedService.AttachToolTip($"This user has shared {sharedData!.Count} Character Data Sets with you." + UiSharedService.TooltipSeparator + + "Click to open the Character Data Hub and show the entries."); + + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + _mediator.Publish(new OpenCharaDataHubWithFilterMessage(_pair.UserData)); + } + ImGui.SameLine(); + } + + if (individualAnimDisabled || individualSoundsDisabled) + { + ImGui.SetCursorPosY(textPosY); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow); + _uiSharedService.IconText(permIcon); + ImGui.PopStyleColor(); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + + ImGui.TextUnformatted("Individual User permissions"); + + if (individualSoundsDisabled) + { + var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID; + _uiSharedService.IconText(FontAwesomeIcon.VolumeOff); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(userSoundsText); + ImGui.NewLine(); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableSounds() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableSounds() ? "Disabled" : "Enabled")); + } + + if (individualAnimDisabled) + { + var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID; + _uiSharedService.IconText(FontAwesomeIcon.Stop); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(userAnimText); + ImGui.NewLine(); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableAnimations() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableAnimations() ? "Disabled" : "Enabled")); + } + + if (individualVFXDisabled) + { + var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID; + _uiSharedService.IconText(FontAwesomeIcon.Circle); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(userVFXText); + ImGui.NewLine(); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableVFX() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableVFX() ? "Disabled" : "Enabled")); + } + + ImGui.EndTooltip(); + } + ImGui.SameLine(); + } + else if ((animDisabled || soundsDisabled)) + { + ImGui.SetCursorPosY(textPosY); + _uiSharedService.IconText(permIcon); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + + ImGui.TextUnformatted("Syncshell User permissions"); + + if (soundsDisabled) + { + var userSoundsText = "Sound sync disabled by " + _pair.UserData.AliasOrUID; + _uiSharedService.IconText(FontAwesomeIcon.VolumeOff); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(userSoundsText); + } + + if (animDisabled) + { + var userAnimText = "Animation sync disabled by " + _pair.UserData.AliasOrUID; + _uiSharedService.IconText(FontAwesomeIcon.Stop); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(userAnimText); + } + + if (vfxDisabled) + { + var userVFXText = "VFX sync disabled by " + _pair.UserData.AliasOrUID; + _uiSharedService.IconText(FontAwesomeIcon.Circle); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(userVFXText); + } + + ImGui.EndTooltip(); + } + ImGui.SameLine(); + } + + if (showPlus) + { + ImGui.SetCursorPosY(originalY); + + if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) + { + _ = _apiController.UserAddPair(new UserDto(new(_pair.UserData.UID))); + } + UiSharedService.AttachToolTip("Pair with " + entryUID + " individually"); + ImGui.SameLine(); + } + + if (showBars) + { + ImGui.SetCursorPosY(originalY); + + if (_uiSharedService.IconButton(FontAwesomeIcon.Bars)) + { + ImGui.OpenPopup("Popup"); + } + } + + if (ImGui.BeginPopup("Popup")) + { + if ((userIsModerator || userIsOwner) && !(entryIsMod || entryIsOwner)) + { + var pinText = entryIsPinned ? "Unpin user" : "Pin user"; + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Thumbtack, pinText)) + { + ImGui.CloseCurrentPopup(); + var userInfo = _fullInfoDto.GroupPairStatusInfo ^ GroupUserInfo.IsPinned; + _ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(_fullInfoDto.Group, _fullInfoDto.User, userInfo)); + } + UiSharedService.AttachToolTip("Pin this user to the Syncshell. Pinned users will not be deleted in case of a manually initiated Syncshell clean"); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove user") && UiSharedService.CtrlPressed()) + { + ImGui.CloseCurrentPopup(); + _ = _apiController.GroupRemoveUser(_fullInfoDto); + } + + UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " from Syncshell"); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User")) + { + ImGui.CloseCurrentPopup(); + _mediator.Publish(new OpenBanUserPopupMessage(_pair, _group)); + } + UiSharedService.AttachToolTip("Ban user from this Syncshell"); + } + + if (userIsOwner) + { + string modText = entryIsMod ? "Demod user" : "Mod user"; + if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserShield, modText) && UiSharedService.CtrlPressed()) + { + ImGui.CloseCurrentPopup(); + var userInfo = _fullInfoDto.GroupPairStatusInfo ^ GroupUserInfo.IsModerator; + _ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(_fullInfoDto.Group, _fullInfoDto.User, userInfo)); + } + UiSharedService.AttachToolTip("Hold CTRL to change the moderator status for " + (_fullInfoDto.UserAliasOrUID) + Environment.NewLine + + "Moderators can kick, ban/unban, pin/unpin users and clear the Syncshell."); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Crown, "Transfer Ownership") && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed()) + { + ImGui.CloseCurrentPopup(); + _ = _apiController.GroupChangeOwnership(_fullInfoDto); + } + UiSharedService.AttachToolTip("Hold CTRL and SHIFT and click to transfer ownership of this Syncshell to " + (_fullInfoDto.UserAliasOrUID) + Environment.NewLine + "WARNING: This action is irreversible."); + } + + if (userIsOwner || (userIsModerator && !(entryIsMod || entryIsOwner))) + ImGui.Separator(); + + if (_pair.IsVisible) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Eye, "Target player")) + { + _mediator.Publish(new TargetPairMessage(_pair)); + ImGui.CloseCurrentPopup(); + } + } + if (!_pair.IsPaused) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.User, "Open Profile")) + { + _displayHandler.OpenProfile(_pair); + ImGui.CloseCurrentPopup(); + } + } + if (_pair.IsVisible) + { +#if DEBUG + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Open Analysis")) + { + _displayHandler.OpenAnalysis(_pair); + ImGui.CloseCurrentPopup(); + } +#endif + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Reload last data")) + { + _pair.ApplyLastReceivedData(forced: true); + ImGui.CloseCurrentPopup(); + } + UiSharedService.AttachToolTip("This reapplies the last received character data to this character"); + } + ImGui.EndPopup(); + } + + return pos - spacing; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/DrawPairBase.cs b/MareSynchronos/UI/Components/DrawPairBase.cs new file mode 100644 index 0000000..54513b7 --- /dev/null +++ b/MareSynchronos/UI/Components/DrawPairBase.cs @@ -0,0 +1,65 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.UI.Handlers; +using MareSynchronos.WebAPI; + +namespace MareSynchronos.UI.Components; + +public abstract class DrawPairBase +{ + protected readonly ApiController _apiController; + protected readonly UidDisplayHandler _displayHandler; + protected readonly UiSharedService _uiSharedService; + protected Pair _pair; + private readonly string _id; + + protected DrawPairBase(string id, Pair entry, ApiController apiController, UidDisplayHandler uIDDisplayHandler, UiSharedService uiSharedService) + { + _id = id; + _pair = entry; + _apiController = apiController; + _displayHandler = uIDDisplayHandler; + _uiSharedService = uiSharedService; + } + + public string ImGuiID => _id; + public string UID => _pair.UserData.UID; + + public void DrawPairedClient() + { + var originalY = ImGui.GetCursorPosY(); + var pauseIconSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Play); + var textSize = ImGui.CalcTextSize(_pair.UserData.AliasOrUID); + + var startPos = ImGui.GetCursorStartPos(); + + var framePadding = ImGui.GetStyle().FramePadding; + var lineHeight = textSize.Y + framePadding.Y * 2; + + var off = startPos.Y; + var height = UiSharedService.GetWindowContentRegionHeight(); + + if ((originalY + off) < -lineHeight || (originalY + off) > height) + { + ImGui.Dummy(new System.Numerics.Vector2(0f, lineHeight)); + return; + } + + var textPosY = originalY + pauseIconSize.Y / 2 - textSize.Y / 2; + DrawLeftSide(textPosY, originalY); + ImGui.SameLine(); + var posX = ImGui.GetCursorPosX(); + var rightSide = DrawRightSide(textPosY, originalY); + DrawName(originalY, posX, rightSide); + } + + protected abstract void DrawLeftSide(float textPosY, float originalY); + + protected abstract float DrawRightSide(float textPosY, float originalY); + + private void DrawName(float originalY, float leftSide, float rightSide) + { + _displayHandler.DrawPairText(_id, _pair, leftSide, originalY, () => rightSide - leftSide); + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/DrawUserPair.cs b/MareSynchronos/UI/Components/DrawUserPair.cs new file mode 100644 index 0000000..23989b7 --- /dev/null +++ b/MareSynchronos/UI/Components/DrawUserPair.cs @@ -0,0 +1,306 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.User; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.UI.Handlers; +using MareSynchronos.WebAPI; +using System.Numerics; + +namespace MareSynchronos.UI.Components; + +public class DrawUserPair : DrawPairBase +{ + protected readonly MareMediator _mediator; + private readonly SelectGroupForPairUi _selectGroupForPairUi; + private readonly CharaDataManager _charaDataManager; + + public DrawUserPair(string id, Pair entry, UidDisplayHandler displayHandler, ApiController apiController, + MareMediator mareMediator, SelectGroupForPairUi selectGroupForPairUi, + UiSharedService uiSharedService, CharaDataManager charaDataManager) + : base(id, entry, apiController, displayHandler, uiSharedService) + { + if (_pair.UserPair == null) throw new ArgumentException("Pair must be UserPair", nameof(entry)); + _pair = entry; + _selectGroupForPairUi = selectGroupForPairUi; + _mediator = mareMediator; + _charaDataManager = charaDataManager; + } + + public bool IsOnline => _pair.IsOnline; + public bool IsVisible => _pair.IsVisible; + public UserPairDto UserPair => _pair.UserPair!; + + protected override void DrawLeftSide(float textPosY, float originalY) + { + FontAwesomeIcon connectionIcon; + Vector4 connectionColor; + string connectionText; + if (!(_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired())) + { + connectionIcon = FontAwesomeIcon.ArrowUp; + connectionText = _pair.UserData.AliasOrUID + " has not added you back"; + connectionColor = ImGuiColors.DalamudRed; + } + else if (_pair.UserPair!.OwnPermissions.IsPaused() || _pair.UserPair!.OtherPermissions.IsPaused()) + { + connectionIcon = FontAwesomeIcon.PauseCircle; + connectionText = "Pairing status with " + _pair.UserData.AliasOrUID + " is paused"; + connectionColor = ImGuiColors.DalamudYellow; + } + else + { + connectionIcon = FontAwesomeIcon.Check; + connectionText = "You are paired with " + _pair.UserData.AliasOrUID; + connectionColor = ImGuiColors.ParsedGreen; + } + + ImGui.SetCursorPosY(textPosY); + ImGui.PushFont(UiBuilder.IconFont); + UiSharedService.ColorText(connectionIcon.ToIconString(), connectionColor); + ImGui.PopFont(); + UiSharedService.AttachToolTip(connectionText); + if (_pair is { IsOnline: true, IsVisible: true }) + { + ImGui.SameLine(); + ImGui.SetCursorPosY(textPosY); + ImGui.PushFont(UiBuilder.IconFont); + UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), ImGuiColors.ParsedGreen); + if (ImGui.IsItemClicked()) + { + _mediator.Publish(new TargetPairMessage(_pair)); + } + ImGui.PopFont(); + var visibleTooltip = _pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName! + Environment.NewLine + "Click to target this player"; + if (_pair.LastAppliedDataBytes >= 0) + { + visibleTooltip += UiSharedService.TooltipSeparator; + visibleTooltip += ((!_pair.IsVisible) ? "(Last) " : string.Empty) + "Mods Info" + Environment.NewLine; + visibleTooltip += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataBytes, true); + if (_pair.LastAppliedApproximateVRAMBytes >= 0) + { + visibleTooltip += Environment.NewLine + "Approx. VRAM Usage: " + UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes, true); + } + if (_pair.LastAppliedDataTris >= 0) + { + visibleTooltip += Environment.NewLine + "Triangle Count (excl. Vanilla): " + + (_pair.LastAppliedDataTris > 1000 ? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'") : _pair.LastAppliedDataTris); + } + } + + UiSharedService.AttachToolTip(visibleTooltip); + } + } + + protected override float DrawRightSide(float textPosY, float originalY) + { + var pauseIcon = _pair.UserPair!.OwnPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + var pauseIconSize = _uiSharedService.GetIconButtonSize(pauseIcon); + var barButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars); + var entryUID = _pair.UserData.AliasOrUID; + var spacingX = ImGui.GetStyle().ItemSpacing.X; + var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth(); + var rightSidePos = windowEndX - barButtonSize.X; + + // Flyout Menu + ImGui.SameLine(rightSidePos); + ImGui.SetCursorPosY(originalY); + + if (_uiSharedService.IconButton(FontAwesomeIcon.Bars)) + { + ImGui.OpenPopup("User Flyout Menu"); + } + if (ImGui.BeginPopup("User Flyout Menu")) + { + using (ImRaii.PushId($"buttons-{_pair.UserData.UID}")) DrawPairedClientMenu(_pair); + ImGui.EndPopup(); + } + + // Pause (mutual pairs only) + if (_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired()) + { + rightSidePos -= pauseIconSize.X + spacingX; + ImGui.SameLine(rightSidePos); + ImGui.SetCursorPosY(originalY); + if (_uiSharedService.IconButton(pauseIcon)) + { + var perm = _pair.UserPair!.OwnPermissions; + perm.SetPaused(!perm.IsPaused()); + _ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm)); + } + UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused() + ? "Pause pairing with " + entryUID + : "Resume pairing with " + entryUID); + + + var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false); + var individualAnimDisabled = (_pair.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableAnimations() ?? false); + var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false); + + // Icon for individually applied permissions + if (individualSoundsDisabled || individualAnimDisabled || individualVFXDisabled) + { + var icon = FontAwesomeIcon.ExclamationTriangle; + var iconwidth = _uiSharedService.GetIconButtonSize(icon); + + rightSidePos -= iconwidth.X + spacingX / 2f; + ImGui.SameLine(rightSidePos); + + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow); + _uiSharedService.IconText(icon); + ImGui.PopStyleColor(); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + + ImGui.TextUnformatted("Individual User permissions"); + + if (individualSoundsDisabled) + { + var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID; + _uiSharedService.IconText(FontAwesomeIcon.VolumeOff); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(userSoundsText); + ImGui.NewLine(); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableSounds() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableSounds() ? "Disabled" : "Enabled")); + } + + if (individualAnimDisabled) + { + var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID; + _uiSharedService.IconText(FontAwesomeIcon.Stop); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(userAnimText); + ImGui.NewLine(); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableAnimations() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableAnimations() ? "Disabled" : "Enabled")); + } + + if (individualVFXDisabled) + { + var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID; + _uiSharedService.IconText(FontAwesomeIcon.Circle); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(userVFXText); + ImGui.NewLine(); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted("You: " + (_pair.UserPair!.OwnPermissions.IsDisableVFX() ? "Disabled" : "Enabled") + ", They: " + (_pair.UserPair!.OtherPermissions.IsDisableVFX() ? "Disabled" : "Enabled")); + } + + ImGui.EndTooltip(); + } + } + } + + // Icon for shared character data + if (_charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData)) + { + var icon = FontAwesomeIcon.Running; + var iconwidth = _uiSharedService.GetIconButtonSize(icon); + rightSidePos -= iconwidth.X + spacingX / 2f; + ImGui.SameLine(rightSidePos); + _uiSharedService.IconText(icon); + + UiSharedService.AttachToolTip($"This user has shared {sharedData.Count} Character Data Sets with you." + UiSharedService.TooltipSeparator + + "Click to open the Character Data Hub and show the entries."); + + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + _mediator.Publish(new OpenCharaDataHubWithFilterMessage(_pair.UserData)); + } + } + + return rightSidePos - spacingX; + } + + private void DrawPairedClientMenu(Pair entry) + { + if (entry.IsVisible) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Eye, "Target player")) + { + _mediator.Publish(new TargetPairMessage(entry)); + ImGui.CloseCurrentPopup(); + } + } + if (!entry.IsPaused) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.User, "Open Profile")) + { + _displayHandler.OpenProfile(entry); + ImGui.CloseCurrentPopup(); + } + UiSharedService.AttachToolTip("Opens the profile for this user in a new window"); + } + if (entry.IsVisible) + { +#if DEBUG + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Open Analysis")) + { + _displayHandler.OpenAnalysis(_pair); + ImGui.CloseCurrentPopup(); + } +#endif + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Reload last data")) + { + entry.ApplyLastReceivedData(forced: true); + ImGui.CloseCurrentPopup(); + } + UiSharedService.AttachToolTip("This reapplies the last received character data to this character"); + } + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state")) + { + _ = _apiController.CyclePause(entry.UserData); + ImGui.CloseCurrentPopup(); + } + var entryUID = entry.UserData.AliasOrUID; + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Folder, "Pair Groups")) + { + _selectGroupForPairUi.Open(entry); + } + UiSharedService.AttachToolTip("Choose pair groups for " + entryUID); + + var isDisableSounds = entry.UserPair!.OwnPermissions.IsDisableSounds(); + string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync"; + var disableSoundsIcon = isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute; + if (_uiSharedService.IconTextButton(disableSoundsIcon, disableSoundsText)) + { + var permissions = entry.UserPair.OwnPermissions; + permissions.SetDisableSounds(!isDisableSounds); + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); + } + + var isDisableAnims = entry.UserPair!.OwnPermissions.IsDisableAnimations(); + string disableAnimsText = isDisableAnims ? "Enable animation sync" : "Disable animation sync"; + var disableAnimsIcon = isDisableAnims ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop; + if (_uiSharedService.IconTextButton(disableAnimsIcon, disableAnimsText)) + { + var permissions = entry.UserPair.OwnPermissions; + permissions.SetDisableAnimations(!isDisableAnims); + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); + } + + var isDisableVFX = entry.UserPair!.OwnPermissions.IsDisableVFX(); + string disableVFXText = isDisableVFX ? "Enable VFX sync" : "Disable VFX sync"; + var disableVFXIcon = isDisableVFX ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle; + if (_uiSharedService.IconTextButton(disableVFXIcon, disableVFXText)) + { + var permissions = entry.UserPair.OwnPermissions; + permissions.SetDisableVFX(!isDisableVFX); + _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); + } + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Unpair Permanently") && UiSharedService.CtrlPressed()) + { + _ = _apiController.UserRemovePair(new(entry.UserData)); + } + UiSharedService.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID); + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/GroupPanel.cs b/MareSynchronos/UI/Components/GroupPanel.cs new file mode 100644 index 0000000..067d591 --- /dev/null +++ b/MareSynchronos/UI/Components/GroupPanel.cs @@ -0,0 +1,703 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Comparer; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI.Components; +using MareSynchronos.UI.Handlers; +using MareSynchronos.WebAPI; +using System.Globalization; +using System.Numerics; + +namespace MareSynchronos.UI; + +internal sealed class GroupPanel +{ + private readonly Dictionary _expandedGroupState = new(StringComparer.Ordinal); + private readonly CompactUi _mainUi; + private readonly PairManager _pairManager; + private readonly ChatService _chatService; + private readonly MareConfigService _mareConfig; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly CharaDataManager _charaDataManager; + private readonly Dictionary _showGidForEntry = new(StringComparer.Ordinal); + private readonly UidDisplayHandler _uidDisplayHandler; + private readonly UiSharedService _uiShared; + private List _bannedUsers = new(); + private int _bulkInviteCount = 10; + private List _bulkOneTimeInvites = new(); + private string _editGroupComment = string.Empty; + private string _editGroupEntry = string.Empty; + private bool _errorGroupCreate = false; + private bool _errorGroupJoin; + private bool _isPasswordValid; + private GroupPasswordDto? _lastCreatedGroup = null; + private bool _modalBanListOpened; + private bool _modalBulkOneTimeInvitesOpened; + private bool _modalChangePwOpened; + private string _newSyncShellPassword = string.Empty; + private bool _showModalBanList = false; + private bool _showModalBulkOneTimeInvites = false; + private bool _showModalChangePassword; + private bool _showModalCreateGroup; + private bool _showModalEnterPassword; + private string _syncShellPassword = string.Empty; + private string _syncShellToJoin = string.Empty; + + public GroupPanel(CompactUi mainUi, UiSharedService uiShared, PairManager pairManager, ChatService chatServivce, + UidDisplayHandler uidDisplayHandler, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager, + CharaDataManager charaDataManager) + { + _mainUi = mainUi; + _uiShared = uiShared; + _pairManager = pairManager; + _chatService = chatServivce; + _uidDisplayHandler = uidDisplayHandler; + _mareConfig = mareConfig; + _serverConfigurationManager = serverConfigurationManager; + _charaDataManager = charaDataManager; + } + + private ApiController ApiController => _uiShared.ApiController; + + public void DrawSyncshells() + { + using (ImRaii.PushId("addsyncshell")) DrawAddSyncshell(); + using (ImRaii.PushId("syncshelllist")) DrawSyncshellList(); + _mainUi.TransferPartHeight = ImGui.GetCursorPosY(); + } + + private void DrawAddSyncshell() + { + var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Plus); + ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); + ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias (leave empty to create)", ref _syncShellToJoin, 20); + ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); + + bool userCanJoinMoreGroups = _pairManager.GroupPairs.Count < ApiController.ServerInfo.MaxGroupsJoinedByUser; + bool userCanCreateMoreGroups = _pairManager.GroupPairs.Count(u => string.Equals(u.Key.Owner.UID, ApiController.UID, StringComparison.Ordinal)) < ApiController.ServerInfo.MaxGroupsCreatedByUser; + bool alreadyInGroup = _pairManager.GroupPairs.Select(p => p.Key).Any(p => string.Equals(p.Group.Alias, _syncShellToJoin, StringComparison.Ordinal) + || string.Equals(p.Group.GID, _syncShellToJoin, StringComparison.Ordinal)); + + if (alreadyInGroup) ImGui.BeginDisabled(); + if (_uiShared.IconButton(FontAwesomeIcon.Plus)) + { + if (!string.IsNullOrEmpty(_syncShellToJoin)) + { + if (userCanJoinMoreGroups) + { + _errorGroupJoin = false; + _showModalEnterPassword = true; + ImGui.OpenPopup("Enter Syncshell Password"); + } + } + else + { + if (userCanCreateMoreGroups) + { + _lastCreatedGroup = null; + _errorGroupCreate = false; + _showModalCreateGroup = true; + ImGui.OpenPopup("Create Syncshell"); + } + } + } + UiSharedService.AttachToolTip(_syncShellToJoin.IsNullOrEmpty() + ? (userCanCreateMoreGroups ? "Create Syncshell" : $"You cannot create more than {ApiController.ServerInfo.MaxGroupsCreatedByUser} Syncshells") + : (userCanJoinMoreGroups ? "Join Syncshell" + _syncShellToJoin : $"You cannot join more than {ApiController.ServerInfo.MaxGroupsJoinedByUser} Syncshells")); + + if (alreadyInGroup) ImGui.EndDisabled(); + + if (ImGui.BeginPopupModal("Enter Syncshell Password", ref _showModalEnterPassword, UiSharedService.PopupWindowFlags)) + { + UiSharedService.TextWrapped("Before joining any Syncshells please be aware that you will be automatically paired with everyone in the Syncshell."); + ImGui.Separator(); + UiSharedService.TextWrapped("Enter the password for Syncshell " + _syncShellToJoin + ":"); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##password", _syncShellToJoin + " Password", ref _syncShellPassword, 255, ImGuiInputTextFlags.Password); + if (_errorGroupJoin) + { + UiSharedService.ColorTextWrapped($"An error occured during joining of this Syncshell: you either have joined the maximum amount of Syncshells ({ApiController.ServerInfo.MaxGroupsJoinedByUser}), " + + $"it does not exist, the password you entered is wrong, you already joined the Syncshell, the Syncshell is full ({ApiController.ServerInfo.MaxGroupUserCount} users) or the Syncshell has closed invites.", + new Vector4(1, 0, 0, 1)); + } + if (ImGui.Button("Join " + _syncShellToJoin)) + { + var shell = _syncShellToJoin; + var pw = _syncShellPassword; + _errorGroupJoin = !ApiController.GroupJoin(new(new GroupData(shell), pw)).Result; + if (!_errorGroupJoin) + { + _syncShellToJoin = string.Empty; + _showModalEnterPassword = false; + } + _syncShellPassword = string.Empty; + } + UiSharedService.SetScaledWindowSize(290); + ImGui.EndPopup(); + } + + if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags)) + { + UiSharedService.TextWrapped("Press the button below to create a new Syncshell."); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + if (ImGui.Button("Create Syncshell")) + { + try + { + _lastCreatedGroup = ApiController.GroupCreate().Result; + } + catch + { + _lastCreatedGroup = null; + _errorGroupCreate = true; + } + } + + if (_lastCreatedGroup != null) + { + ImGui.Separator(); + _errorGroupCreate = false; + ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.Group.GID); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password); + ImGui.SameLine(); + if (_uiShared.IconButton(FontAwesomeIcon.Copy)) + { + ImGui.SetClipboardText(_lastCreatedGroup.Password); + } + UiSharedService.TextWrapped("You can change the Syncshell password later at any time."); + } + + if (_errorGroupCreate) + { + UiSharedService.ColorTextWrapped("You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells.", + new Vector4(1, 0, 0, 1)); + } + + UiSharedService.SetScaledWindowSize(350); + ImGui.EndPopup(); + } + + ImGuiHelpers.ScaledDummy(2); + } + + private void DrawSyncshell(GroupFullInfoDto groupDto, List pairsInGroup) + { + int shellNumber = _serverConfigurationManager.GetShellNumberForGid(groupDto.GID); + + var name = groupDto.Group.Alias ?? groupDto.GID; + if (!_expandedGroupState.TryGetValue(groupDto.GID, out bool isExpanded)) + { + isExpanded = false; + _expandedGroupState.Add(groupDto.GID, isExpanded); + } + + var icon = isExpanded ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight; + _uiShared.IconText(icon); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + _expandedGroupState[groupDto.GID] = !_expandedGroupState[groupDto.GID]; + } + ImGui.SameLine(); + + var textIsGid = true; + string groupName = groupDto.GroupAliasOrGID; + + if (string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal)) + { + ImGui.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(FontAwesomeIcon.Crown.ToIconString()); + ImGui.PopFont(); + UiSharedService.AttachToolTip("You are the owner of Syncshell " + groupName); + ImGui.SameLine(); + } + else if (groupDto.GroupUserInfo.IsModerator()) + { + ImGui.PushFont(UiBuilder.IconFont); + ImGui.TextUnformatted(FontAwesomeIcon.UserShield.ToIconString()); + ImGui.PopFont(); + UiSharedService.AttachToolTip("You are a moderator of Syncshell " + groupName); + ImGui.SameLine(); + } + + _showGidForEntry.TryGetValue(groupDto.GID, out var showGidInsteadOfName); + var groupComment = _serverConfigurationManager.GetNoteForGid(groupDto.GID); + if (!showGidInsteadOfName && !string.IsNullOrEmpty(groupComment)) + { + groupName = groupComment; + textIsGid = false; + } + + if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal)) + { + var shellConfig = _serverConfigurationManager.GetShellConfigForGid(groupDto.GID); + if (!_mareConfig.Current.DisableSyncshellChat && shellConfig.Enabled) + { + ImGui.TextUnformatted($"[{shellNumber}]"); + UiSharedService.AttachToolTip("Chat command prefix: /ss" + shellNumber); + } + if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont); + ImGui.SameLine(); + ImGui.TextUnformatted(groupName); + if (textIsGid) ImGui.PopFont(); + UiSharedService.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine + + "Right click to change comment for " + groupName + Environment.NewLine + Environment.NewLine + + "Users: " + (pairsInGroup.Count + 1) + ", Owner: " + groupDto.OwnerAliasOrUID); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + var prevState = textIsGid; + if (_showGidForEntry.ContainsKey(groupDto.GID)) + { + prevState = _showGidForEntry[groupDto.GID]; + } + + _showGidForEntry[groupDto.GID] = !prevState; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _serverConfigurationManager.SetNoteForGid(_editGroupEntry, _editGroupComment); + _editGroupComment = _serverConfigurationManager.GetNoteForGid(groupDto.GID) ?? string.Empty; + _editGroupEntry = groupDto.GID; + _chatService.MaybeUpdateShellName(shellNumber); + } + } + else + { + var buttonSizes = _uiShared.GetIconButtonSize(FontAwesomeIcon.Bars).X + _uiShared.GetIconButtonSize(FontAwesomeIcon.LockOpen).X; + ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX() - buttonSizes - ImGui.GetStyle().ItemSpacing.X * 2); + if (ImGui.InputTextWithHint("", "Comment/Notes", ref _editGroupComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) + { + _serverConfigurationManager.SetNoteForGid(groupDto.GID, _editGroupComment); + _editGroupEntry = string.Empty; + _chatService.MaybeUpdateShellName(shellNumber); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _editGroupEntry = string.Empty; + } + UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel"); + } + + + using (ImRaii.PushId(groupDto.GID + "settings")) DrawSyncShellButtons(groupDto, pairsInGroup); + + if (_showModalBanList && !_modalBanListOpened) + { + _modalBanListOpened = true; + ImGui.OpenPopup("Manage Banlist for " + groupDto.GID); + } + + if (!_showModalBanList) _modalBanListOpened = false; + + if (ImGui.BeginPopupModal("Manage Banlist for " + groupDto.GID, ref _showModalBanList, UiSharedService.PopupWindowFlags)) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) + { + _bannedUsers = ApiController.GroupGetBannedUsers(groupDto).Result; + } + + if (ImGui.BeginTable("bannedusertable" + groupDto.GID, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollY)) + { + 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(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Check, "Unban#" + bannedUser.UID)) + { + _ = ApiController.GroupUnbanUser(bannedUser); + _bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal)); + } + } + + ImGui.EndTable(); + } + UiSharedService.SetScaledWindowSize(700, 300); + ImGui.EndPopup(); + } + + if (_showModalChangePassword && !_modalChangePwOpened) + { + _modalChangePwOpened = true; + ImGui.OpenPopup("Change Syncshell Password"); + } + + if (!_showModalChangePassword) _modalChangePwOpened = false; + + if (ImGui.BeginPopupModal("Change Syncshell Password", ref _showModalChangePassword, UiSharedService.PopupWindowFlags)) + { + UiSharedService.TextWrapped("Enter the new Syncshell password for Syncshell " + name + " here."); + UiSharedService.TextWrapped("This action is irreversible"); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##changepw", "New password for " + name, ref _newSyncShellPassword, 255); + if (ImGui.Button("Change password")) + { + var pw = _newSyncShellPassword; + _isPasswordValid = ApiController.GroupChangePassword(new(groupDto.Group, pw)).Result; + _newSyncShellPassword = string.Empty; + if (_isPasswordValid) _showModalChangePassword = false; + } + + if (!_isPasswordValid) + { + UiSharedService.ColorTextWrapped("The selected password is too short. It must be at least 10 characters.", new Vector4(1, 0, 0, 1)); + } + + UiSharedService.SetScaledWindowSize(290); + ImGui.EndPopup(); + } + + if (_showModalBulkOneTimeInvites && !_modalBulkOneTimeInvitesOpened) + { + _modalBulkOneTimeInvitesOpened = true; + ImGui.OpenPopup("Create Bulk One-Time Invites"); + } + + if (!_showModalBulkOneTimeInvites) _modalBulkOneTimeInvitesOpened = false; + + if (ImGui.BeginPopupModal("Create Bulk One-Time Invites", ref _showModalBulkOneTimeInvites, UiSharedService.PopupWindowFlags)) + { + UiSharedService.TextWrapped("This allows you to create up to 100 one-time invites at once for the Syncshell " + name + "." + Environment.NewLine + + "The invites are valid for 24h after creation and will automatically expire."); + ImGui.Separator(); + if (_bulkOneTimeInvites.Count == 0) + { + ImGui.SetNextItemWidth(-1); + ImGui.SliderInt("Amount##bulkinvites", ref _bulkInviteCount, 1, 100); + if (_uiShared.IconTextButton(FontAwesomeIcon.MailBulk, "Create invites")) + { + _bulkOneTimeInvites = ApiController.GroupCreateTempInvite(groupDto, _bulkInviteCount).Result; + } + } + else + { + UiSharedService.TextWrapped("A total of " + _bulkOneTimeInvites.Count + " invites have been created."); + if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "Copy invites to clipboard")) + { + ImGui.SetClipboardText(string.Join(Environment.NewLine, _bulkOneTimeInvites)); + } + } + + UiSharedService.SetScaledWindowSize(290); + ImGui.EndPopup(); + } + + bool hideOfflineUsers = pairsInGroup.Count > 1000; + + ImGui.Indent(20); + if (_expandedGroupState[groupDto.GID]) + { + var sortedPairs = pairsInGroup + .OrderByDescending(u => string.Equals(u.UserData.UID, groupDto.OwnerUID, StringComparison.Ordinal)) + .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsModerator()) + .ThenByDescending(u => u.GroupPair[groupDto].GroupPairStatusInfo.IsPinned()) + .ThenBy(u => u.GetPairSortKey(), StringComparer.OrdinalIgnoreCase); + + var visibleUsers = new List(); + var onlineUsers = new List(); + var offlineUsers = new List(); + + foreach (var pair in sortedPairs) + { + var drawPair = new DrawGroupPair( + groupDto.GID + pair.UserData.UID, pair, + ApiController, _mainUi.Mediator, groupDto, + pair.GroupPair.Single( + g => GroupDataComparer.Instance.Equals(g.Key.Group, groupDto.Group) + ).Value, + _uidDisplayHandler, + _uiShared, + _charaDataManager); + + if (pair.IsVisible) + visibleUsers.Add(drawPair); + else if (pair.IsOnline) + onlineUsers.Add(drawPair); + else + offlineUsers.Add(drawPair); + } + + if (visibleUsers.Count > 0) + { + ImGui.TextUnformatted("Visible"); + ImGui.Separator(); + _uidDisplayHandler.RenderPairList(visibleUsers); + } + + if (onlineUsers.Count > 0) + { + ImGui.TextUnformatted("Online"); + ImGui.Separator(); + _uidDisplayHandler.RenderPairList(onlineUsers); + } + + if (offlineUsers.Count > 0) + { + ImGui.TextUnformatted("Offline/Unknown"); + ImGui.Separator(); + if (hideOfflineUsers) + { + UiSharedService.ColorText($" {offlineUsers.Count} offline users omitted from display.", ImGuiColors.DalamudGrey); + } + else + { + _uidDisplayHandler.RenderPairList(offlineUsers); + } + } + + ImGui.Separator(); + } + ImGui.Unindent(20); + } + + private void DrawSyncShellButtons(GroupFullInfoDto groupDto, List groupPairs) + { + var infoIcon = FontAwesomeIcon.InfoCircle; + + bool invitesEnabled = !groupDto.GroupPermissions.IsDisableInvites(); + var soundsDisabled = groupDto.GroupPermissions.IsDisableSounds(); + var animDisabled = groupDto.GroupPermissions.IsDisableAnimations(); + var vfxDisabled = groupDto.GroupPermissions.IsDisableVFX(); + + var userSoundsDisabled = groupDto.GroupUserPermissions.IsDisableSounds(); + var userAnimDisabled = groupDto.GroupUserPermissions.IsDisableAnimations(); + var userVFXDisabled = groupDto.GroupUserPermissions.IsDisableVFX(); + + bool showInfoIcon = !invitesEnabled || soundsDisabled || animDisabled || vfxDisabled || userSoundsDisabled || userAnimDisabled || userVFXDisabled; + + var lockedIcon = invitesEnabled ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock; + var animIcon = animDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running; + var soundsIcon = soundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; + var vfxIcon = vfxDisabled ? FontAwesomeIcon.Circle : FontAwesomeIcon.Sun; + var userAnimIcon = userAnimDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running; + var userSoundsIcon = userSoundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; + var userVFXIcon = userVFXDisabled ? FontAwesomeIcon.Circle : FontAwesomeIcon.Sun; + + var iconSize = UiSharedService.GetIconSize(infoIcon); + var barbuttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Bars); + var isOwner = string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal); + + var spacingX = ImGui.GetStyle().ItemSpacing.X; + var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth(); + var pauseIcon = groupDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + var pauseIconSize = _uiShared.GetIconButtonSize(pauseIcon); + + ImGui.SameLine(windowEndX - barbuttonSize.X - (showInfoIcon ? iconSize.X : 0) - (showInfoIcon ? spacingX : 0) - pauseIconSize.X - spacingX); + + if (showInfoIcon) + { + _uiShared.IconText(infoIcon); + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + if (!invitesEnabled || soundsDisabled || animDisabled || vfxDisabled) + { + ImGui.TextUnformatted("Syncshell permissions"); + + if (!invitesEnabled) + { + var lockedText = "Syncshell is closed for joining"; + _uiShared.IconText(lockedIcon); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(lockedText); + } + + if (soundsDisabled) + { + var soundsText = "Sound sync disabled through owner"; + _uiShared.IconText(soundsIcon); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(soundsText); + } + + if (animDisabled) + { + var animText = "Animation sync disabled through owner"; + _uiShared.IconText(animIcon); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(animText); + } + + if (vfxDisabled) + { + var vfxText = "VFX sync disabled through owner"; + _uiShared.IconText(vfxIcon); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(vfxText); + } + } + + if (userSoundsDisabled || userAnimDisabled || userVFXDisabled) + { + if (!invitesEnabled || soundsDisabled || animDisabled || vfxDisabled) + ImGui.Separator(); + + ImGui.TextUnformatted("Your permissions"); + + if (userSoundsDisabled) + { + var userSoundsText = "Sound sync disabled through you"; + _uiShared.IconText(userSoundsIcon); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(userSoundsText); + } + + if (userAnimDisabled) + { + var userAnimText = "Animation sync disabled through you"; + _uiShared.IconText(userAnimIcon); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(userAnimText); + } + + if (userVFXDisabled) + { + var userVFXText = "VFX sync disabled through you"; + _uiShared.IconText(userVFXIcon); + ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); + ImGui.TextUnformatted(userVFXText); + } + + if (!invitesEnabled || soundsDisabled || animDisabled || vfxDisabled) + UiSharedService.TextWrapped("Note that syncshell permissions for disabling take precedence over your own set permissions"); + } + ImGui.EndTooltip(); + } + ImGui.SameLine(); + } + + if (_uiShared.IconButton(pauseIcon)) + { + var userPerm = groupDto.GroupUserPermissions ^ GroupUserPermissions.Paused; + _ = ApiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(groupDto.Group, new UserData(ApiController.UID), userPerm)); + } + UiSharedService.AttachToolTip((groupDto.GroupUserPermissions.IsPaused() ? "Resume" : "Pause") + " pairing with all users in this Syncshell"); + ImGui.SameLine(); + + if (_uiShared.IconButton(FontAwesomeIcon.Bars)) + { + ImGui.OpenPopup("ShellPopup"); + } + + if (ImGui.BeginPopup("ShellPopup")) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowCircleLeft, "Leave Syncshell") && UiSharedService.CtrlPressed()) + { + _ = ApiController.GroupLeave(groupDto); + } + UiSharedService.AttachToolTip("Hold CTRL and click to leave this Syncshell" + (!string.Equals(groupDto.OwnerUID, ApiController.UID, StringComparison.Ordinal) ? string.Empty : Environment.NewLine + + "WARNING: This action is irreversible" + Environment.NewLine + "Leaving an owned Syncshell will transfer the ownership to a random person in the Syncshell.")); + + if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "Copy ID")) + { + ImGui.CloseCurrentPopup(); + ImGui.SetClipboardText(groupDto.GroupAliasOrGID); + } + UiSharedService.AttachToolTip("Copy Syncshell ID to Clipboard"); + + if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Copy Notes")) + { + ImGui.CloseCurrentPopup(); + ImGui.SetClipboardText(UiSharedService.GetNotes(groupPairs)); + } + UiSharedService.AttachToolTip("Copies all your notes for all users in this Syncshell to the clipboard." + Environment.NewLine + "They can be imported via Settings -> General -> Notes -> Import notes from clipboard"); + + var soundsText = userSoundsDisabled ? "Enable sound sync" : "Disable sound sync"; + if (_uiShared.IconTextButton(userSoundsIcon, soundsText)) + { + ImGui.CloseCurrentPopup(); + var perm = groupDto.GroupUserPermissions; + perm.SetDisableSounds(!perm.IsDisableSounds()); + _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); + } + UiSharedService.AttachToolTip("Sets your allowance for sound synchronization for users of this syncshell." + + Environment.NewLine + "Disabling the synchronization will stop applying sound modifications for users of this syncshell." + + Environment.NewLine + "Note: this setting can be forcefully overridden to 'disabled' through the syncshell owner." + + Environment.NewLine + "Note: this setting does not apply to individual pairs that are also in the syncshell."); + + var animText = userAnimDisabled ? "Enable animations sync" : "Disable animations sync"; + if (_uiShared.IconTextButton(userAnimIcon, animText)) + { + ImGui.CloseCurrentPopup(); + var perm = groupDto.GroupUserPermissions; + perm.SetDisableAnimations(!perm.IsDisableAnimations()); + _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); + } + UiSharedService.AttachToolTip("Sets your allowance for animations synchronization for users of this syncshell." + + Environment.NewLine + "Disabling the synchronization will stop applying animations modifications for users of this syncshell." + + Environment.NewLine + "Note: this setting might also affect sound synchronization" + + Environment.NewLine + "Note: this setting can be forcefully overridden to 'disabled' through the syncshell owner." + + Environment.NewLine + "Note: this setting does not apply to individual pairs that are also in the syncshell."); + + var vfxText = userVFXDisabled ? "Enable VFX sync" : "Disable VFX sync"; + if (_uiShared.IconTextButton(userVFXIcon, vfxText)) + { + ImGui.CloseCurrentPopup(); + var perm = groupDto.GroupUserPermissions; + perm.SetDisableVFX(!perm.IsDisableVFX()); + _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); + } + UiSharedService.AttachToolTip("Sets your allowance for VFX synchronization for users of this syncshell." + + Environment.NewLine + "Disabling the synchronization will stop applying VFX modifications for users of this syncshell." + + Environment.NewLine + "Note: this setting might also affect animation synchronization to some degree" + + Environment.NewLine + "Note: this setting can be forcefully overridden to 'disabled' through the syncshell owner." + + Environment.NewLine + "Note: this setting does not apply to individual pairs that are also in the syncshell."); + + if (isOwner || groupDto.GroupUserInfo.IsModerator()) + { + ImGui.Separator(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Cog, "Open Admin Panel")) + { + ImGui.CloseCurrentPopup(); + _mainUi.Mediator.Publish(new OpenSyncshellAdminPanel(groupDto)); + } + } + + ImGui.EndPopup(); + } + } + + private void DrawSyncshellList() + { + var ySize = _mainUi.TransferPartHeight == 0 + ? 1 + : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - _mainUi.TransferPartHeight - ImGui.GetCursorPosY(); + ImGui.BeginChild("list", new Vector2(_mainUi.WindowContentWidth, ySize), border: false); + foreach (var entry in _pairManager.GroupPairs.OrderBy(g => g.Key.Group.AliasOrGID, StringComparer.OrdinalIgnoreCase).ToList()) + { + using (ImRaii.PushId(entry.Key.Group.GID)) DrawSyncshell(entry.Key, entry.Value); + } + ImGui.EndChild(); + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/PairGroupsUi.cs b/MareSynchronos/UI/Components/PairGroupsUi.cs new file mode 100644 index 0000000..0778a02 --- /dev/null +++ b/MareSynchronos/UI/Components/PairGroupsUi.cs @@ -0,0 +1,258 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.MareConfiguration; +using MareSynchronos.UI.Handlers; +using MareSynchronos.WebAPI; + +namespace MareSynchronos.UI.Components; + +public class PairGroupsUi +{ + private readonly ApiController _apiController; + private readonly MareConfigService _mareConfig; + private readonly SelectPairForGroupUi _selectGroupForPairUi; + private readonly TagHandler _tagHandler; + private readonly UidDisplayHandler _uidDisplayHandler; + private readonly UiSharedService _uiSharedService; + + public PairGroupsUi(MareConfigService mareConfig, TagHandler tagHandler, UidDisplayHandler uidDisplayHandler, ApiController apiController, + SelectPairForGroupUi selectGroupForPairUi, UiSharedService uiSharedService) + { + _mareConfig = mareConfig; + _tagHandler = tagHandler; + _uidDisplayHandler = uidDisplayHandler; + _apiController = apiController; + _selectGroupForPairUi = selectGroupForPairUi; + _uiSharedService = uiSharedService; + } + + public void Draw(List visibleUsers, List onlineUsers, List offlineUsers) where T : DrawPairBase + { + // Only render those tags that actually have pairs in them, otherwise + // we can end up with a bunch of useless pair groups + var tagsWithPairsInThem = _tagHandler.GetAllTagsSorted(); + var allUsers = onlineUsers.Concat(offlineUsers).ToList(); + if (typeof(T) == typeof(DrawUserPair)) + { + DrawUserPairs(tagsWithPairsInThem, allUsers.Cast().ToList(), visibleUsers.Cast(), onlineUsers.Cast(), offlineUsers.Cast()); + } + } + + private void DrawButtons(string tag, List availablePairsInThisTag) + { + var allArePaused = availablePairsInThisTag.All(pair => pair.UserPair!.OwnPermissions.IsPaused()); + var pauseButton = allArePaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + var flyoutMenuX = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X; + var pauseButtonX = _uiSharedService.GetIconButtonSize(pauseButton).X; + var windowX = ImGui.GetWindowContentRegionMin().X; + var windowWidth = UiSharedService.GetWindowContentRegionWidth(); + var spacingX = ImGui.GetStyle().ItemSpacing.X; + + var buttonPauseOffset = windowX + windowWidth - flyoutMenuX - spacingX - pauseButtonX; + ImGui.SameLine(buttonPauseOffset); + if (_uiSharedService.IconButton(pauseButton)) + { + // If all of the currently visible pairs (after applying filters to the pairs) + // are paused we display a resume button to resume all currently visible (after filters) + // pairs. Otherwise, we just pause all the remaining pairs. + if (allArePaused) + { + // If all are paused => resume all + ResumeAllPairs(availablePairsInThisTag); + } + else + { + // otherwise pause all remaining + PauseRemainingPairs(availablePairsInThisTag); + } + } + if (allArePaused) + { + UiSharedService.AttachToolTip($"Resume pairing with all pairs in {tag}"); + } + else + { + UiSharedService.AttachToolTip($"Pause pairing with all pairs in {tag}"); + } + + var buttonDeleteOffset = windowX + windowWidth - flyoutMenuX; + ImGui.SameLine(buttonDeleteOffset); + if (_uiSharedService.IconButton(FontAwesomeIcon.Bars)) + { + ImGui.OpenPopup("Group Flyout Menu"); + } + + if (ImGui.BeginPopup("Group Flyout Menu")) + { + using (ImRaii.PushId($"buttons-{tag}")) DrawGroupMenu(tag); + ImGui.EndPopup(); + } + } + + private void DrawCategory(string tag, IEnumerable onlineUsers, IEnumerable allUsers, IEnumerable? visibleUsers = null) + { + IEnumerable usersInThisTag; + HashSet? otherUidsTaggedWithTag = null; + bool isSpecialTag = false; + int visibleInThisTag = 0; + if (tag is TagHandler.CustomOfflineTag or TagHandler.CustomOnlineTag or TagHandler.CustomVisibleTag or TagHandler.CustomUnpairedTag) + { + usersInThisTag = onlineUsers; + isSpecialTag = true; + } + else + { + otherUidsTaggedWithTag = _tagHandler.GetOtherUidsForTag(tag); + usersInThisTag = onlineUsers + .Where(pair => otherUidsTaggedWithTag.Contains(pair.UID)) + .ToList(); + visibleInThisTag = visibleUsers?.Count(p => otherUidsTaggedWithTag.Contains(p.UID)) ?? 0; + } + + if (isSpecialTag && !usersInThisTag.Any()) return; + + DrawName(tag, isSpecialTag, visibleInThisTag, usersInThisTag.Count(), otherUidsTaggedWithTag?.Count); + if (!isSpecialTag) + { + using (ImRaii.PushId($"group-{tag}-buttons")) DrawButtons(tag, allUsers.Cast().Where(p => otherUidsTaggedWithTag!.Contains(p.UID)).ToList()); + } + else + { + // Avoid uncomfortably close group names + if (!_tagHandler.IsTagOpen(tag)) + { + var size = ImGui.CalcTextSize("").Y + ImGui.GetStyle().FramePadding.Y * 2f; + ImGui.SameLine(); + ImGui.Dummy(new(size, size)); + } + } + + if (!_tagHandler.IsTagOpen(tag)) return; + + ImGui.Indent(20); + DrawPairs(tag, usersInThisTag); + ImGui.Unindent(20); + } + + private void DrawGroupMenu(string tag) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Users, "Add people to " + tag)) + { + _selectGroupForPairUi.Open(tag); + } + UiSharedService.AttachToolTip($"Add more users to Group {tag}"); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete " + tag) && UiSharedService.CtrlPressed()) + { + _tagHandler.RemoveTag(tag); + } + UiSharedService.AttachToolTip($"Delete Group {tag} (Will not delete the pairs)" + Environment.NewLine + "Hold CTRL to delete"); + } + + private void DrawName(string tag, bool isSpecialTag, int visible, int online, int? total) + { + string displayedName = tag switch + { + TagHandler.CustomUnpairedTag => "Unpaired", + TagHandler.CustomOfflineTag => "Offline", + TagHandler.CustomOnlineTag => _mareConfig.Current.ShowOfflineUsersSeparately ? "Online/Paused" : "Contacts", + TagHandler.CustomVisibleTag => "Visible", + _ => tag + }; + + string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)"; + + // FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight + var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight; + _uiSharedService.IconText(icon); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + ToggleTagOpen(tag); + } + ImGui.SameLine(); + ImGui.TextUnformatted(resultFolderName); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + ToggleTagOpen(tag); + } + + if (!isSpecialTag && ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.TextUnformatted($"Group {tag}"); + ImGui.Separator(); + ImGui.TextUnformatted($"{visible} Pairs visible"); + ImGui.TextUnformatted($"{online} Pairs online/paused"); + ImGui.TextUnformatted($"{total} Pairs total"); + ImGui.EndTooltip(); + } + } + + private void DrawPairs(string tag, IEnumerable availablePairsInThisCategory) + { + // These are all the OtherUIDs that are tagged with this tag + _uidDisplayHandler.RenderPairList(availablePairsInThisCategory); + ImGui.Separator(); + } + + private void DrawUserPairs(List tagsWithPairsInThem, List allUsers, IEnumerable visibleUsers, IEnumerable onlineUsers, IEnumerable offlineUsers) + { + if (_mareConfig.Current.ShowVisibleUsersSeparately) + { + using (ImRaii.PushId("$group-VisibleCustomTag")) DrawCategory(TagHandler.CustomVisibleTag, visibleUsers, allUsers); + } + foreach (var tag in tagsWithPairsInThem) + { + if (_mareConfig.Current.ShowOfflineUsersSeparately) + { + using (ImRaii.PushId($"group-{tag}")) DrawCategory(tag, onlineUsers, allUsers, visibleUsers); + } + else + { + using (ImRaii.PushId($"group-{tag}")) DrawCategory(tag, allUsers, allUsers, visibleUsers); + } + } + if (_mareConfig.Current.ShowOfflineUsersSeparately) + { + using (ImRaii.PushId($"group-OnlineCustomTag")) DrawCategory(TagHandler.CustomOnlineTag, + onlineUsers.Where(u => !_tagHandler.HasAnyTag(u.UID)).ToList(), allUsers); + using (ImRaii.PushId($"group-OfflineCustomTag")) DrawCategory(TagHandler.CustomOfflineTag, + offlineUsers.Where(u => u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers); + } + else + { + using (ImRaii.PushId($"group-OnlineCustomTag")) DrawCategory(TagHandler.CustomOnlineTag, + onlineUsers.Concat(offlineUsers.Where(u => u.UserPair!.OtherPermissions.IsPaired())).Where(u => !_tagHandler.HasAnyTag(u.UID)).ToList(), allUsers); + } + using (ImRaii.PushId($"group-UnpairedCustomTag")) DrawCategory(TagHandler.CustomUnpairedTag, + offlineUsers.Where(u => !u.UserPair!.OtherPermissions.IsPaired()).ToList(), allUsers); + } + + private void PauseRemainingPairs(List availablePairs) + { + foreach (var pairToPause in availablePairs.Where(pair => !pair.UserPair!.OwnPermissions.IsPaused())) + { + var perm = pairToPause.UserPair!.OwnPermissions; + perm.SetPaused(paused: true); + _ = _apiController.UserSetPairPermissions(new(new(pairToPause.UID), perm)); + } + } + + private void ResumeAllPairs(List availablePairs) + { + foreach (var pairToPause in availablePairs) + { + var perm = pairToPause.UserPair!.OwnPermissions; + perm.SetPaused(paused: false); + _ = _apiController.UserSetPairPermissions(new(new(pairToPause.UID), perm)); + } + } + + private void ToggleTagOpen(string tag) + { + bool open = !_tagHandler.IsTagOpen(tag); + _tagHandler.SetTagOpen(tag, open); + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs b/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs new file mode 100644 index 0000000..7b21c29 --- /dev/null +++ b/MareSynchronos/UI/Components/Popup/BanUserPopupHandler.cs @@ -0,0 +1,50 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI; +using System.Numerics; + +namespace MareSynchronos.UI.Components.Popup; + +public class BanUserPopupHandler : IPopupHandler +{ + private readonly ApiController _apiController; + private readonly UiSharedService _uiSharedService; + private string _banReason = string.Empty; + private GroupFullInfoDto _group = null!; + private Pair _reportedPair = null!; + + public BanUserPopupHandler(ApiController apiController, UiSharedService uiSharedService) + { + _apiController = apiController; + _uiSharedService = uiSharedService; + } + + public Vector2 PopupSize => new(500, 250); + + public bool ShowClose => true; + + public void DrawContent() + { + UiSharedService.TextWrapped("User " + (_reportedPair.UserData.AliasOrUID) + " will be banned and removed from this Syncshell."); + ImGui.InputTextWithHint("##banreason", "Ban Reason", ref _banReason, 255); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User")) + { + ImGui.CloseCurrentPopup(); + var reason = _banReason; + _ = _apiController.GroupBanUser(new GroupPairDto(_group.Group, _reportedPair.UserData), reason); + _banReason = string.Empty; + } + UiSharedService.TextWrapped("The reason will be displayed in the banlist. The current server-side alias if present (Vanity ID) will automatically be attached to the reason."); + } + + public void Open(OpenBanUserPopupMessage message) + { + _reportedPair = message.PairToBan; + _group = message.GroupFullInfoDto; + _banReason = string.Empty; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/IPopupHandler.cs b/MareSynchronos/UI/Components/Popup/IPopupHandler.cs new file mode 100644 index 0000000..21b99f9 --- /dev/null +++ b/MareSynchronos/UI/Components/Popup/IPopupHandler.cs @@ -0,0 +1,11 @@ +using System.Numerics; + +namespace MareSynchronos.UI.Components.Popup; + +public interface IPopupHandler +{ + Vector2 PopupSize { get; } + bool ShowClose { get; } + + void DrawContent(); +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/PopupHandler.cs b/MareSynchronos/UI/Components/Popup/PopupHandler.cs new file mode 100644 index 0000000..370c3de --- /dev/null +++ b/MareSynchronos/UI/Components/Popup/PopupHandler.cs @@ -0,0 +1,81 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; +using System.Numerics; + +namespace MareSynchronos.UI.Components.Popup; + +public class PopupHandler : WindowMediatorSubscriberBase +{ + protected bool _openPopup = false; + private readonly HashSet _handlers; + private readonly UiSharedService _uiSharedService; + private IPopupHandler? _currentHandler = null; + + public PopupHandler(ILogger logger, MareMediator mediator, IEnumerable popupHandlers, + PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService) + : base(logger, mediator, "MarePopupHandler", performanceCollectorService) + { + Flags = ImGuiWindowFlags.NoBringToFrontOnFocus + | ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.NoInputs + | ImGuiWindowFlags.NoSavedSettings + | ImGuiWindowFlags.NoBackground + | ImGuiWindowFlags.NoMove + | ImGuiWindowFlags.NoNav + | ImGuiWindowFlags.NoTitleBar + | ImGuiWindowFlags.NoFocusOnAppearing; + + IsOpen = true; + + _handlers = popupHandlers.ToHashSet(); + + Mediator.Subscribe(this, (msg) => + { + _openPopup = true; + _currentHandler = _handlers.OfType().Single(); + ((ReportPopupHandler)_currentHandler).Open(msg); + IsOpen = true; + }); + + Mediator.Subscribe(this, (msg) => + { + _openPopup = true; + _currentHandler = _handlers.OfType().Single(); + ((BanUserPopupHandler)_currentHandler).Open(msg); + IsOpen = true; + }); + _uiSharedService = uiSharedService; + DisableWindowSounds = true; + } + + protected override void DrawInternal() + { + if (_currentHandler == null) return; + + if (_openPopup) + { + ImGui.OpenPopup(WindowName); + _openPopup = false; + } + + var viewportSize = ImGui.GetWindowViewport().Size; + ImGui.SetNextWindowSize(_currentHandler!.PopupSize * ImGuiHelpers.GlobalScale); + ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f)); + using var popup = ImRaii.Popup(WindowName, ImGuiWindowFlags.Modal); + if (!popup) return; + _currentHandler.DrawContent(); + if (_currentHandler.ShowClose) + { + ImGui.Separator(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Times, "Close")) + { + ImGui.CloseCurrentPopup(); + } + } + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs b/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs new file mode 100644 index 0000000..659e5e0 --- /dev/null +++ b/MareSynchronos/UI/Components/Popup/ReportPopupHandler.cs @@ -0,0 +1,58 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI; +using System.Numerics; + +namespace MareSynchronos.UI.Components.Popup; + +internal class ReportPopupHandler : IPopupHandler +{ + private readonly ApiController _apiController; + private readonly UiSharedService _uiSharedService; + private Pair? _reportedPair; + private string _reportReason = string.Empty; + + public ReportPopupHandler(ApiController apiController, UiSharedService uiSharedService) + { + _apiController = apiController; + _uiSharedService = uiSharedService; + } + + public Vector2 PopupSize => new(500, 500); + + public bool ShowClose => true; + + public void DrawContent() + { + using (_uiSharedService.UidFont.Push()) + UiSharedService.TextWrapped("Report " + _reportedPair!.UserData.AliasOrUID + " Profile"); + + ImGui.InputTextMultiline("##reportReason", ref _reportReason, 500, new Vector2(500 - ImGui.GetStyle().ItemSpacing.X * 2, 200)); + UiSharedService.TextWrapped($"Note: Sending a report will disable the offending profile globally.{Environment.NewLine}" + + $"The report will be sent to the team of your currently connected server.{Environment.NewLine}" + + $"Depending on the severity of the offense the users profile or account can be permanently disabled or banned."); + UiSharedService.ColorTextWrapped("Report spam and wrong reports will not be tolerated and can lead to permanent account suspension.", ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped("This is not for reporting misbehavior but solely for the actual profile. " + + "Reports that are not solely for the profile will be ignored.", ImGuiColors.DalamudYellow); + + using (ImRaii.Disabled(string.IsNullOrEmpty(_reportReason))) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Send Report")) + { + ImGui.CloseCurrentPopup(); + var reason = _reportReason; + _ = _apiController.UserReportProfile(new(_reportedPair.UserData, reason)); + } + } + } + + public void Open(OpenReportPopupMessage msg) + { + _reportedPair = msg.PairToReport; + _reportReason = string.Empty; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/SelectGroupForPairUi.cs b/MareSynchronos/UI/Components/SelectGroupForPairUi.cs new file mode 100644 index 0000000..a5a1cfd --- /dev/null +++ b/MareSynchronos/UI/Components/SelectGroupForPairUi.cs @@ -0,0 +1,139 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.UI.Handlers; +using System.Numerics; + +namespace MareSynchronos.UI.Components; + +public class SelectGroupForPairUi +{ + private readonly TagHandler _tagHandler; + private readonly UidDisplayHandler _uidDisplayHandler; + private readonly UiSharedService _uiSharedService; + + /// + /// The group UI is always open for a specific pair. This defines which pair the UI is open for. + /// + /// + private Pair? _pair; + + /// + /// Should the panel show, yes/no + /// + private bool _show; + + /// + /// For the add category option, this stores the currently typed in tag name + /// + private string _tagNameToAdd = ""; + + public SelectGroupForPairUi(TagHandler tagHandler, UidDisplayHandler uidDisplayHandler, UiSharedService uiSharedService) + { + _show = false; + _pair = null; + _tagHandler = tagHandler; + _uidDisplayHandler = uidDisplayHandler; + _uiSharedService = uiSharedService; + } + + public void Draw() + { + if (_pair == null) + { + return; + } + + var name = PairName(_pair); + var popupName = $"Choose Groups for {name}"; + // Is the popup supposed to show but did not open yet? Open it + if (_show) + { + ImGui.OpenPopup(popupName); + _show = false; + } + + if (ImGui.BeginPopup(popupName)) + { + var tags = _tagHandler.GetAllTagsSorted(); + var childHeight = tags.Count != 0 ? tags.Count * 25 : 1; + var childSize = new Vector2(0, childHeight > 100 ? 100 : childHeight) * ImGuiHelpers.GlobalScale; + + ImGui.TextUnformatted($"Select the groups you want {name} to be in."); + if (ImGui.BeginChild(name + "##listGroups", childSize)) + { + foreach (var tag in tags) + { + using (ImRaii.PushId($"groups-pair-{_pair.UserData.UID}-{tag}")) DrawGroupName(_pair, tag); + } + ImGui.EndChild(); + } + + ImGui.Separator(); + ImGui.TextUnformatted($"Create a new group for {name}."); + if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) + { + HandleAddTag(); + } + ImGui.SameLine(); + ImGui.InputTextWithHint("##category_name", "New Group", ref _tagNameToAdd, 40); + if (ImGui.IsKeyDown(ImGuiKey.Enter)) + { + HandleAddTag(); + } + ImGui.EndPopup(); + } + } + + public void Open(Pair pair) + { + _pair = pair; + // Using "_show" here to de-couple the opening of the popup + // The popup name is derived from the name the user currently sees, which is + // based on the showUidForEntry dictionary. + // We'd have to derive the name here to open it popup modal here, when the Open() is called + _show = true; + } + + private void DrawGroupName(Pair pair, string name) + { + var hasTagBefore = _tagHandler.HasTag(pair.UserData.UID, name); + var hasTag = hasTagBefore; + if (ImGui.Checkbox(name, ref hasTag)) + { + if (hasTag) + { + _tagHandler.AddTagToPairedUid(pair.UserData.UID, name); + } + else + { + _tagHandler.RemoveTagFromPairedUid(pair.UserData.UID, name); + } + } + } + + private void HandleAddTag() + { + if (!_tagNameToAdd.IsNullOrWhitespace() && _tagNameToAdd is not (TagHandler.CustomOfflineTag or TagHandler.CustomOnlineTag or TagHandler.CustomVisibleTag)) + { + _tagHandler.AddTag(_tagNameToAdd); + if (_pair != null) + { + _tagHandler.AddTagToPairedUid(_pair.UserData.UID, _tagNameToAdd); + } + _tagNameToAdd = string.Empty; + } + else + { + _tagNameToAdd = string.Empty; + } + } + + private string PairName(Pair pair) + { + return _uidDisplayHandler.GetPlayerText(pair).text; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Components/SelectPairForGroupUi.cs b/MareSynchronos/UI/Components/SelectPairForGroupUi.cs new file mode 100644 index 0000000..63da9c3 --- /dev/null +++ b/MareSynchronos/UI/Components/SelectPairForGroupUi.cs @@ -0,0 +1,92 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Utility; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.UI.Handlers; +using System.Numerics; + +namespace MareSynchronos.UI.Components; + +public class SelectPairForGroupUi +{ + private readonly TagHandler _tagHandler; + private readonly UidDisplayHandler _uidDisplayHandler; + private string _filter = string.Empty; + private bool _opened = false; + private HashSet _peopleInGroup = new(StringComparer.Ordinal); + private bool _show = false; + private string _tag = string.Empty; + + public SelectPairForGroupUi(TagHandler tagHandler, UidDisplayHandler uidDisplayHandler) + { + _tagHandler = tagHandler; + _uidDisplayHandler = uidDisplayHandler; + } + + public void Draw(List pairs) + { + var workHeight = ImGui.GetMainViewport().WorkSize.Y / ImGuiHelpers.GlobalScale; + var minSize = new Vector2(300, workHeight < 400 ? workHeight : 400) * ImGuiHelpers.GlobalScale; + var maxSize = new Vector2(300, 1000) * ImGuiHelpers.GlobalScale; + + var popupName = $"Choose Users for Group {_tag}"; + + if (!_show) + { + _opened = false; + } + + if (_show && !_opened) + { + ImGui.SetNextWindowSize(minSize); + UiSharedService.CenterNextWindow(minSize.X, minSize.Y, ImGuiCond.Always); + ImGui.OpenPopup(popupName); + _opened = true; + } + + ImGui.SetNextWindowSizeConstraints(minSize, maxSize); + if (ImGui.BeginPopupModal(popupName, ref _show, ImGuiWindowFlags.Popup | ImGuiWindowFlags.Modal)) + { + ImGui.TextUnformatted($"Select users for group {_tag}"); + + ImGui.InputTextWithHint("##filter", "Filter", ref _filter, 255, ImGuiInputTextFlags.None); + foreach (var item in pairs + .Where(p => string.IsNullOrEmpty(_filter) || PairName(p).Contains(_filter, StringComparison.OrdinalIgnoreCase)) + .OrderBy(p => PairName(p), StringComparer.OrdinalIgnoreCase) + .ToList()) + { + var isInGroup = _peopleInGroup.Contains(item.UserData.UID); + if (ImGui.Checkbox(PairName(item), ref isInGroup)) + { + if (isInGroup) + { + _tagHandler.AddTagToPairedUid(item.UserData.UID, _tag); + _peopleInGroup.Add(item.UserData.UID); + } + else + { + _tagHandler.RemoveTagFromPairedUid(item.UserData.UID, _tag); + _peopleInGroup.Remove(item.UserData.UID); + } + } + } + ImGui.EndPopup(); + } + else + { + _filter = string.Empty; + _show = false; + } + } + + public void Open(string tag) + { + _peopleInGroup = _tagHandler.GetOtherUidsForTag(tag); + _tag = tag; + _show = true; + } + + private string PairName(Pair pair) + { + return _uidDisplayHandler.GetPlayerText(pair).text; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/DataAnalysisUi.cs b/MareSynchronos/UI/DataAnalysisUi.cs new file mode 100644 index 0000000..15add8f --- /dev/null +++ b/MareSynchronos/UI/DataAnalysisUi.cs @@ -0,0 +1,492 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; +using System.Numerics; + +namespace MareSynchronos.UI; + +public class DataAnalysisUi : WindowMediatorSubscriberBase +{ + private readonly CharacterAnalyzer _characterAnalyzer; + private readonly Progress<(string, int)> _conversionProgress = new(); + private readonly IpcManager _ipcManager; + private readonly UiSharedService _uiSharedService; + private readonly Dictionary _texturesToConvert = new(StringComparer.Ordinal); + private Dictionary>? _cachedAnalysis; + private CancellationTokenSource _conversionCancellationTokenSource = new(); + private string _conversionCurrentFileName = string.Empty; + private int _conversionCurrentFileProgress = 0; + private Task? _conversionTask; + private bool _enableBc7ConversionMode = false; + private bool _hasUpdate = false; + private bool _sortDirty = true; + private bool _modalOpen = false; + private string _selectedFileTypeTab = string.Empty; + private string _selectedHash = string.Empty; + private ObjectKind _selectedObjectTab; + private bool _showModal = false; + + public DataAnalysisUi(ILogger logger, MareMediator mediator, + CharacterAnalyzer characterAnalyzer, IpcManager ipcManager, + PerformanceCollectorService performanceCollectorService, + UiSharedService uiSharedService) + : base(logger, mediator, "Character Data Analysis", performanceCollectorService) + { + _characterAnalyzer = characterAnalyzer; + _ipcManager = ipcManager; + _uiSharedService = uiSharedService; + Mediator.Subscribe(this, (_) => + { + _hasUpdate = true; + }); + SizeConstraints = new() + { + MinimumSize = new() + { + X = 800, + Y = 600 + }, + MaximumSize = new() + { + X = 3840, + Y = 2160 + } + }; + + _conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged; + } + + protected override void DrawInternal() + { + if (_conversionTask != null && !_conversionTask.IsCompleted) + { + _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; + } + } + else if (_conversionTask != null && _conversionTask.IsCompleted && _texturesToConvert.Count > 0) + { + _conversionTask = null; + _texturesToConvert.Clear(); + _showModal = false; + _modalOpen = false; + _enableBc7ConversionMode = false; + } + + if (_showModal && !_modalOpen) + { + ImGui.OpenPopup("BC7 Conversion in Progress"); + _modalOpen = true; + } + + if (_hasUpdate) + { + _cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone(); + _hasUpdate = false; + _sortDirty = true; + } + + UiSharedService.TextWrapped("This window shows you all files and their sizes that are currently in use through your character and associated entities"); + + if (_cachedAnalysis == null || _cachedAnalysis.Count == 0) return; + + bool isAnalyzing = _characterAnalyzer.IsAnalysisRunning; + bool needAnalysis = _cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed)); + if (isAnalyzing) + { + UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}", + ImGuiColors.DalamudYellow); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis")) + { + _characterAnalyzer.CancelAnalyze(); + } + } + else + { + if (needAnalysis) + { + UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data", + ImGuiColors.DalamudYellow); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)")) + { + _ = _characterAnalyzer.ComputeAnalysis(print: false); + } + } + else + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (recalculate all entries)")) + { + _ = _characterAnalyzer.ComputeAnalysis(print: false, recalculate: true); + } + } + } + + 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 (download size):"); + ImGui.SameLine(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis)) + { + ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize)))); + if (needAnalysis && !isAnalyzing) + { + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TextUnformatted(FontAwesomeIcon.ExclamationCircle.ToIconString()); + UiSharedService.AttachToolTip("Click \"Start analysis\" to calculate download size"); + } + } + ImGui.TextUnformatted($"Total modded model triangles: {UiSharedService.TrisToString(_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles)))}"); + ImGui.Separator(); + + using var tabbar = ImRaii.TabBar("objectSelection"); + foreach (var kvp in _cachedAnalysis) + { + using var id = ImRaii.PushId(kvp.Key.ToString()); + string tabText = kvp.Key.ToString(); + using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString()); + if (tab.Success) + { + var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal) + .OrderBy(k => k.Key, StringComparer.Ordinal).ToList(); + + ImGui.TextUnformatted("Files for " + kvp.Key); + ImGui.SameLine(); + 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 = ""; + text = string.Join(Environment.NewLine, groupedfiles + .Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize)) + + ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize)))); + ImGui.SetTooltip(text); + } + ImGui.TextUnformatted($"{kvp.Key} size (actual):"); + ImGui.SameLine(); + ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize))); + ImGui.TextUnformatted($"{kvp.Key} size (download size):"); + ImGui.SameLine(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis)) + { + ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize))); + if (needAnalysis && !isAnalyzing) + { + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TextUnformatted(FontAwesomeIcon.ExclamationCircle.ToIconString()); + UiSharedService.AttachToolTip("Click \"Start analysis\" to calculate download size"); + } + } + ImGui.TextUnformatted($"{kvp.Key} VRAM usage:"); + ImGui.SameLine(); + var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal)); + if (vramUsage != null) + { + ImGui.TextUnformatted(UiSharedService.ByteToString(vramUsage.Sum(f => f.OriginalSize))); + } + ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {UiSharedService.TrisToString(kvp.Value.Sum(f => f.Value.Triangles))}"); + + ImGui.Separator(); + if (_selectedObjectTab != kvp.Key) + { + _selectedHash = string.Empty; + _selectedObjectTab = kvp.Key; + _selectedFileTypeTab = string.Empty; + _enableBc7ConversionMode = false; + _texturesToConvert.Clear(); + } + + using var fileTabBar = ImRaii.TabBar("fileTabs"); + + foreach (IGrouping? fileGroup in groupedfiles) + { + string fileGroupText = fileGroup.Key + " [" + fileGroup.Count() + "]"; + var requiresCompute = fileGroup.Any(k => !k.IsComputed); + using var tabcol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(ImGuiColors.DalamudYellow), requiresCompute); + 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 (download size):"); + 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:", ImGuiColors.DalamudYellow); + 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." + , ImGuiColors.DalamudYellow); + 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(); + } + } + } + + ImGui.Separator(); + + ImGui.TextUnformatted("Selected file:"); + ImGui.SameLine(); + UiSharedService.ColorText(_selectedHash, ImGuiColors.DalamudYellow); + + if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item)) + { + var filePaths = item.FilePaths; + ImGui.TextUnformatted("Local file path:"); + ImGui.SameLine(); + UiSharedService.TextWrapped(filePaths[0]); + if (filePaths.Count > 1) + { + ImGui.SameLine(); + ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)"); + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); + UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1))); + } + + var gamepaths = item.GamePaths; + ImGui.TextUnformatted("Used by game path:"); + ImGui.SameLine(); + UiSharedService.TextWrapped(gamepaths[0]); + if (gamepaths.Count > 1) + { + ImGui.SameLine(); + ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)"); + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); + UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1))); + } + } + } + + public override void OnOpen() + { + _hasUpdate = true; + _selectedHash = string.Empty; + _enableBc7ConversionMode = false; + _texturesToConvert.Clear(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged; + } + + private void ConversionProgress_ProgressChanged(object? sender, (string, int) e) + { + _conversionCurrentFileName = e.Item1; + _conversionCurrentFileProgress = e.Item2; + } + + 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; + ImGui.TableSetupColumn("Hash"); + ImGui.TableSetupColumn("Filepaths", ImGuiTableColumnFlags.PreferSortDescending); + ImGui.TableSetupColumn("Gamepaths", ImGuiTableColumnFlags.PreferSortDescending); + ImGui.TableSetupColumn("File Size", ImGuiTableColumnFlags.DefaultSort | ImGuiTableColumnFlags.PreferSortDescending); + ImGui.TableSetupColumn("Download Size", ImGuiTableColumnFlags.PreferSortDescending); + 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", ImGuiTableColumnFlags.PreferSortDescending); + } + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + + var sortSpecs = ImGui.TableGetSortSpecs(); + if (sortSpecs.SpecsDirty || _sortDirty) + { + var idx = sortSpecs.Specs.ColumnIndex; + + 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); + + sortSpecs.SpecsDirty = false; + _sortDirty = 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); + ImGui.TableNextColumn(); + if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal)) + { + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudYellow)); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudYellow)); + } + ImGui.TextUnformatted(item.Hash); + if (ImGui.IsItemClicked()) _selectedHash = 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(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, !item.IsComputed)) + 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 (item.Format.Value.StartsWith("BC", StringComparison.Ordinal) || item.Format.Value.StartsWith("DXT", StringComparison.Ordinal) + || item.Format.Value.StartsWith("24864", StringComparison.Ordinal)) // BC4 + { + ImGui.TextUnformatted(""); + continue; + } + var filePath = item.FilePaths[0]; + bool toConvert = _texturesToConvert.ContainsKey(filePath); + if (ImGui.Checkbox("###convert" + item.Hash, ref toConvert)) + { + 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(UiSharedService.TrisToString(item.Triangles)); + if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + } + } + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/DownloadUi.cs b/MareSynchronos/UI/DownloadUi.cs new file mode 100644 index 0000000..237b2bc --- /dev/null +++ b/MareSynchronos/UI/DownloadUi.cs @@ -0,0 +1,248 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Colors; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI.Files; +using MareSynchronos.WebAPI.Files.Models; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Numerics; + +namespace MareSynchronos.UI; + +public class DownloadUi : WindowMediatorSubscriberBase +{ + private readonly MareConfigService _configService; + private readonly ConcurrentDictionary> _currentDownloads = new(); + private readonly DalamudUtilService _dalamudUtilService; + private readonly FileUploadManager _fileTransferManager; + private readonly UiSharedService _uiShared; + private readonly ConcurrentDictionary _uploadingPlayers = new(); + + public DownloadUi(ILogger logger, DalamudUtilService dalamudUtilService, MareConfigService configService, + FileUploadManager fileTransferManager, MareMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService) + : base(logger, mediator, "Snowcloak Downloads", performanceCollectorService) + { + _dalamudUtilService = dalamudUtilService; + _configService = configService; + _fileTransferManager = fileTransferManager; + _uiShared = uiShared; + + SizeConstraints = new WindowSizeConstraints() + { + MaximumSize = new Vector2(500, 90), + MinimumSize = new Vector2(500, 90), + }; + + Flags |= ImGuiWindowFlags.NoMove; + Flags |= ImGuiWindowFlags.NoBackground; + Flags |= ImGuiWindowFlags.NoInputs; + Flags |= ImGuiWindowFlags.NoNavFocus; + Flags |= ImGuiWindowFlags.NoResize; + Flags |= ImGuiWindowFlags.NoScrollbar; + Flags |= ImGuiWindowFlags.NoTitleBar; + Flags |= ImGuiWindowFlags.NoDecoration; + Flags |= ImGuiWindowFlags.NoFocusOnAppearing; + + DisableWindowSounds = true; + + ForceMainWindow = true; + + IsOpen = true; + + Mediator.Subscribe(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); + Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); + Mediator.Subscribe(this, (_) => IsOpen = false); + Mediator.Subscribe(this, (_) => IsOpen = true); + Mediator.Subscribe(this, (msg) => + { + if (msg.IsUploading) + { + _uploadingPlayers[msg.Handler] = true; + } + else + { + _uploadingPlayers.TryRemove(msg.Handler, out _); + } + }); + } + + protected override void DrawInternal() + { + if (_configService.Current.ShowTransferWindow) + { + try + { + if (_fileTransferManager.CurrentUploads.Any()) + { + var currentUploads = _fileTransferManager.CurrentUploads.ToList(); + 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 + { + // ignore errors thrown from UI + } + + try + { + 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 + { + // ignore errors thrown from UI + } + } + + 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, 255, 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(100, 100, 255, transparency), 1); + + 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); + } + } + + 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(255, 255, 0, transparency), + UiSharedService.Color(0, 0, 0, transparency), 2); + } + catch + { + // ignore errors thrown on UI + } + } + } + } + } + + public override bool DrawConditions() + { + if (_uiShared.EditTrackerPosition) return true; + if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false; + if (!_currentDownloads.Any() && !_fileTransferManager.CurrentUploads.Any() && !_uploadingPlayers.Any()) return false; + if (!IsOpen) return false; + return true; + } + + public override void PreDraw() + { + base.PreDraw(); + + if (_uiShared.EditTrackerPosition) + { + Flags &= ~ImGuiWindowFlags.NoMove; + Flags &= ~ImGuiWindowFlags.NoBackground; + Flags &= ~ImGuiWindowFlags.NoInputs; + Flags &= ~ImGuiWindowFlags.NoResize; + } + else + { + Flags |= ImGuiWindowFlags.NoMove; + Flags |= ImGuiWindowFlags.NoBackground; + Flags |= ImGuiWindowFlags.NoInputs; + Flags |= ImGuiWindowFlags.NoResize; + } + + var maxHeight = ImGui.GetTextLineHeight() * (_configService.Current.ParallelDownloads + 3); + SizeConstraints = new() + { + MinimumSize = new Vector2(300, maxHeight), + MaximumSize = new Vector2(300, maxHeight), + }; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/DtrEntry.cs b/MareSynchronos/UI/DtrEntry.cs new file mode 100644 index 0000000..72b89dc --- /dev/null +++ b/MareSynchronos/UI/DtrEntry.cs @@ -0,0 +1,241 @@ +using Dalamud.Game.Gui.Dtr; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Plugin.Services; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Configurations; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Runtime.InteropServices; + +namespace MareSynchronos.UI; + +public sealed class DtrEntry : IDisposable, IHostedService +{ + private enum DtrStyle + { + Default, + Style1, + Style2, + Style3, + Style4, + Style5, + Style6, + Style7, + Style8, + Style9 + } + + public const int NumStyles = 10; + + private readonly ApiController _apiController; + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly MareConfigService _configService; + private readonly IDtrBar _dtrBar; + private readonly Lazy _entry; + private readonly ILogger _logger; + private readonly MareMediator _mareMediator; + private readonly PairManager _pairManager; + private Task? _runTask; + private string? _text; + private string? _tooltip; + private Colors _colors; + + public DtrEntry(ILogger logger, IDtrBar dtrBar, MareConfigService configService, MareMediator mareMediator, PairManager pairManager, ApiController apiController) + { + _logger = logger; + _dtrBar = dtrBar; + _entry = new(CreateEntry); + _configService = configService; + _mareMediator = mareMediator; + _pairManager = pairManager; + _apiController = apiController; + } + + public void Dispose() + { + if (_entry.IsValueCreated) + { + _logger.LogDebug("Disposing DtrEntry"); + Clear(); + _entry.Value.Remove(); + } + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting DtrEntry"); + _runTask = Task.Run(RunAsync, _cancellationTokenSource.Token); + _logger.LogInformation("Started DtrEntry"); + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _cancellationTokenSource.Cancel(); + try + { + await _runTask!.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // ignore cancelled + } + finally + { + _cancellationTokenSource.Dispose(); + } + } + + private void Clear() + { + if (!_entry.IsValueCreated) return; + _logger.LogInformation("Clearing entry"); + _text = null; + _tooltip = null; + _colors = default; + + _entry.Value.Shown = false; + } + + private IDtrBarEntry CreateEntry() + { + _logger.LogTrace("Creating new DtrBar entry"); + var entry = _dtrBar.Get("Snowcloak"); + entry.OnClick = _ => _mareMediator.Publish(new UiToggleMessage(typeof(CompactUi))); + + return entry; + } + + private async Task RunAsync() + { + while (!_cancellationTokenSource.IsCancellationRequested) + { + await Task.Delay(1000, _cancellationTokenSource.Token).ConfigureAwait(false); + + Update(); + } + } + + private void Update() + { + if (!_configService.Current.EnableDtrEntry || !_configService.Current.HasValidSetup()) + { + if (_entry.IsValueCreated && _entry.Value.Shown) + { + _logger.LogInformation("Disabling entry"); + + Clear(); + } + return; + } + + if (!_entry.Value.Shown) + { + _logger.LogInformation("Showing entry"); + _entry.Value.Shown = true; + } + + string text; + string tooltip; + Colors colors; + if (_apiController.IsConnected) + { + var pairCount = _pairManager.GetVisibleUserCount(); + + text = RenderDtrStyle(_configService.Current.DtrStyle, pairCount.ToString()); + if (pairCount > 0) + { + IEnumerable visiblePairs; + if (_configService.Current.ShowUidInDtrTooltip) + { + visiblePairs = _pairManager.GetOnlineUserPairs() + .Where(x => x.IsVisible) + .Select(x => string.Format("{0} ({1})", _configService.Current.PreferNoteInDtrTooltip ? x.GetNoteOrName() : x.PlayerName, x.UserData.AliasOrUID)); + } + else + { + visiblePairs = _pairManager.GetOnlineUserPairs() + .Where(x => x.IsVisible) + .Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNoteOrName() : x.PlayerName)); + } + + tooltip = $"Snowcloak: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}"; + colors = _configService.Current.DtrColorsPairsInRange; + } + else + { + tooltip = "Snowcloak: Connected"; + colors = _configService.Current.DtrColorsDefault; + } + } + else + { + text = RenderDtrStyle(_configService.Current.DtrStyle, "\uE04C"); + tooltip = "Snowcloak: Not Connected"; + colors = _configService.Current.DtrColorsNotConnected; + } + + if (!_configService.Current.UseColorsInDtr) + colors = default; + + if (!string.Equals(text, _text, StringComparison.Ordinal) || !string.Equals(tooltip, _tooltip, StringComparison.Ordinal) || colors != _colors) + { + _text = text; + _tooltip = tooltip; + _colors = colors; + _entry.Value.Text = BuildColoredSeString(text, colors); + _entry.Value.Tooltip = tooltip; + } + } + + public static string RenderDtrStyle(int styleNum, string text) + { + var style = (DtrStyle)styleNum; + + return style switch { + DtrStyle.Style1 => $"\xE039 {text}", + DtrStyle.Style2 => $"\xE0BC {text}", + DtrStyle.Style3 => $"\xE0BD {text}", + DtrStyle.Style4 => $"\xE03A {text}", + DtrStyle.Style5 => $"\xE033 {text}", + DtrStyle.Style6 => $"\xE038 {text}", + DtrStyle.Style7 => $"\xE05D {text}", + DtrStyle.Style8 => $"\xE03C{text}", + DtrStyle.Style9 => $"\xE040 {text} \xE041", + _ => $"\uE044 {text}" + }; + } + + #region Colored SeString + private const byte _colorTypeForeground = 0x13; + private const byte _colorTypeGlow = 0x14; + + private static SeString BuildColoredSeString(string text, Colors colors) + { + var ssb = new SeStringBuilder(); + if (colors.Foreground != default) + ssb.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground)); + if (colors.Glow != default) + ssb.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow)); + ssb.AddText(text); + if (colors.Glow != default) + ssb.Add(BuildColorEndPayload(_colorTypeGlow)); + if (colors.Foreground != default) + ssb.Add(BuildColorEndPayload(_colorTypeForeground)); + return ssb.Build(); + } + + private static RawPayload BuildColorStartPayload(byte colorType, uint color) + => new(unchecked([0x02, colorType, 0x05, 0xF6, byte.Max((byte)color, 0x01), byte.Max((byte)(color >> 8), 0x01), byte.Max((byte)(color >> 16), 0x01), 0x03])); + + private static RawPayload BuildColorEndPayload(byte colorType) + => new([0x02, colorType, 0x02, 0xEC, 0x03]); + + [StructLayout(LayoutKind.Sequential)] + public readonly record struct Colors(uint Foreground = default, uint Glow = default); + #endregion +} diff --git a/MareSynchronos/UI/EditProfileUi.cs b/MareSynchronos/UI/EditProfileUi.cs new file mode 100644 index 0000000..190aafd --- /dev/null +++ b/MareSynchronos/UI/EditProfileUi.cs @@ -0,0 +1,220 @@ +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 MareSynchronos.API.Data; +using MareSynchronos.API.Dto.User; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.UI; + +public class EditProfileUi : WindowMediatorSubscriberBase +{ + private readonly ApiController _apiController; + private readonly FileDialogManager _fileDialogManager; + private readonly MareProfileManager _mareProfileManager; + private readonly UiSharedService _uiSharedService; + private readonly ServerConfigurationManager _serverConfigurationManager; + private bool _adjustedForScollBarsLocalProfile = false; + private bool _adjustedForScollBarsOnlineProfile = false; + private string _descriptionText = string.Empty; + private IDalamudTextureWrap? _pfpTextureWrap; + private string _profileDescription = string.Empty; + private byte[] _profileImage = []; + private bool _showFileDialogError = false; + private bool _wasOpen; + + public EditProfileUi(ILogger logger, MareMediator mediator, + ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager, + ServerConfigurationManager serverConfigurationManager, + MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService) + : base(logger, mediator, "Snowcloak Edit Profile###SnowcloakSyncEditProfileUI", performanceCollectorService) + { + IsOpen = false; + this.SizeConstraints = new() + { + MinimumSize = new(768, 512), + MaximumSize = new(768, 2000) + }; + _apiController = apiController; + _uiSharedService = uiSharedService; + _fileDialogManager = fileDialogManager; + _serverConfigurationManager = serverConfigurationManager; + _mareProfileManager = mareProfileManager; + + Mediator.Subscribe(this, (_) => { _wasOpen = IsOpen; IsOpen = false; }); + Mediator.Subscribe(this, (_) => IsOpen = _wasOpen); + Mediator.Subscribe(this, (_) => IsOpen = false); + Mediator.Subscribe(this, (msg) => + { + if (msg.UserData == null || string.Equals(msg.UserData.UID, _apiController.UID, StringComparison.Ordinal)) + { + _pfpTextureWrap?.Dispose(); + _pfpTextureWrap = null; + } + }); + } + + protected override void DrawInternal() + { + _uiSharedService.BigText("Current Profile (as saved on server)"); + + var profile = _mareProfileManager.GetMareProfile(new UserData(_apiController.UID)); + + if (profile.IsFlagged) + { + UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed); + return; + } + + if (!_profileImage.SequenceEqual(profile.ImageData.Value)) + { + _profileImage = profile.ImageData.Value; + _pfpTextureWrap?.Dispose(); + _pfpTextureWrap = _uiSharedService.LoadImage(_profileImage); + } + + if (!string.Equals(_profileDescription, profile.Description, StringComparison.OrdinalIgnoreCase)) + { + _profileDescription = profile.Description; + _descriptionText = _profileDescription; + } + + if (_pfpTextureWrap != null) + { + ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height)); + } + + var spacing = ImGui.GetStyle().ItemSpacing.X; + ImGuiHelpers.ScaledRelativeSameLine(256, spacing); + using (_uiSharedService.GameFont.Push()) + { + var descriptionTextSize = ImGui.CalcTextSize(profile.Description, hideTextAfterDoubleHash: false, 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(); + + ImGui.Separator(); + _uiSharedService.BigText("Profile Settings"); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) + { + _fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) => + { + if (!success) return; + _ = Task.Run(async () => + { + var fileContent = File.ReadAllBytes(file); + using MemoryStream ms = new(fileContent); + var format = PngHdr.TryExtractDimensions(ms); + + if (format.Width > 256 || format.Height > 256 || (fileContent.Length > 250 * 1024)) + { + _showFileDialogError = true; + return; + } + + _showFileDialogError = false; + await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null)) + .ConfigureAwait(false); + }); + }); + } + UiSharedService.AttachToolTip("Select and upload a new profile picture"); + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture")) + { + _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null)); + } + UiSharedService.AttachToolTip("Clear your currently uploaded profile picture"); + if (_showFileDialogError) + { + UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed); + } + var isNsfw = profile.IsNSFW; + if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) + { + _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null)); + } + _uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON"); + var widthTextBox = 400; + var posX = ImGui.GetCursorPosX(); + ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500"); + ImGui.SetCursorPosX(posX); + ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X); + ImGui.TextUnformatted("Preview (approximate)"); + using (_uiSharedService.GameFont.Push()) + ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200)); + + ImGui.SameLine(); + + using (_uiSharedService.GameFont.Push()) + { + var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, hideTextAfterDoubleHash: false, 256f); + var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200); + if (descriptionTextSizeLocal.Y > childFrameLocal.Y) + { + _adjustedForScollBarsLocalProfile = true; + } + else + { + _adjustedForScollBarsLocalProfile = false; + } + childFrameLocal = childFrameLocal with + { + X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0), + }; + if (ImGui.BeginChildFrame(102, childFrameLocal)) + { + UiSharedService.TextWrapped(_descriptionText); + } + ImGui.EndChildFrame(); + } + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description")) + { + _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText)); + } + UiSharedService.AttachToolTip("Sets your profile description text"); + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description")) + { + _ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, "")); + } + UiSharedService.AttachToolTip("Clears your profile description text"); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _pfpTextureWrap?.Dispose(); + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/EventViewerUI.cs b/MareSynchronos/UI/EventViewerUI.cs new file mode 100644 index 0000000..5ff52ee --- /dev/null +++ b/MareSynchronos/UI/EventViewerUI.cs @@ -0,0 +1,238 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services; +using MareSynchronos.Services.Events; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.Globalization; +using System.Numerics; + +namespace MareSynchronos.UI; + +internal class EventViewerUI : WindowMediatorSubscriberBase +{ + private readonly EventAggregator _eventAggregator; + private readonly UiSharedService _uiSharedService; + private readonly MareConfigService _configService; + private List _currentEvents = new(); + private Lazy> _filteredEvents; + private string _filterFreeText = string.Empty; + private bool _isPaused = false; + + private List CurrentEvents + { + get + { + return _currentEvents; + } + set + { + _currentEvents = value; + _filteredEvents = RecreateFilter(); + } + } + + public EventViewerUI(ILogger logger, MareMediator mediator, + EventAggregator eventAggregator, UiSharedService uiSharedService, MareConfigService configService, + PerformanceCollectorService performanceCollectorService) + : base(logger, mediator, "Event Viewer", performanceCollectorService) + { + _eventAggregator = eventAggregator; + _uiSharedService = uiSharedService; + _configService = configService; + SizeConstraints = new() + { + MinimumSize = new(700, 400) + }; + _filteredEvents = RecreateFilter(); + } + + private Lazy> RecreateFilter() + { + return new(() => + CurrentEvents.Where(f => + string.IsNullOrEmpty(_filterFreeText) + || (f.EventSource.Contains(_filterFreeText, StringComparison.OrdinalIgnoreCase) + || f.Character.Contains(_filterFreeText, StringComparison.OrdinalIgnoreCase) + || f.UID.Contains(_filterFreeText, StringComparison.OrdinalIgnoreCase) + || f.Message.Contains(_filterFreeText, StringComparison.OrdinalIgnoreCase) + ) + ).ToList()); + } + + private void ClearFilters() + { + _filterFreeText = string.Empty; + _filteredEvents = RecreateFilter(); + } + + public override void OnOpen() + { + CurrentEvents = _eventAggregator.EventList.Value.OrderByDescending(f => f.EventTime).ToList(); + ClearFilters(); + } + + protected override void DrawInternal() + { + var newEventsAvailable = _eventAggregator.NewEventsAvailable; + + var freezeSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.PlayCircle, "Unfreeze View"); + if (_isPaused) + { + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, newEventsAvailable)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Unfreeze View")) + _isPaused = false; + if (newEventsAvailable) + UiSharedService.AttachToolTip("New events are available. Click to resume updating."); + } + } + else + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PauseCircle, "Freeze View")) + _isPaused = true; + } + + if (newEventsAvailable && !_isPaused) + CurrentEvents = _eventAggregator.EventList.Value.OrderByDescending(f => f.EventTime).ToList(); + + ImGui.SameLine(freezeSize + ImGui.GetStyle().ItemSpacing.X * 2); + + bool changedFilter = false; + ImGui.SetNextItemWidth(200); + changedFilter |= ImGui.InputText("Filter lines", ref _filterFreeText, 50); + if (changedFilter) _filteredEvents = RecreateFilter(); + + using (ImRaii.Disabled(_filterFreeText.IsNullOrEmpty())) + { + ImGui.SameLine(); + if (_uiSharedService.IconButton(FontAwesomeIcon.Ban)) + { + _filterFreeText = string.Empty; + _filteredEvents = RecreateFilter(); + } + } + + if (_configService.Current.LogEvents) + { + var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.FolderOpen, "Open EventLog Folder"); + var dist = ImGui.GetWindowContentRegionMax().X - buttonSize; + ImGui.SameLine(dist); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.FolderOpen, "Open EventLog folder")) + { + ProcessStartInfo ps = new() + { + FileName = _eventAggregator.EventLogFolder, + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Normal + }; + Process.Start(ps); + } + } + + var cursorPos = ImGui.GetCursorPosY(); + var max = ImGui.GetWindowContentRegionMax(); + var min = ImGui.GetWindowContentRegionMin(); + var width = max.X - min.X; + var height = max.Y - cursorPos; + using var table = ImRaii.Table("eventTable", 6, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg, + new Vector2(width, height)); + + float timeColWidth = ImGui.CalcTextSize("88:88:88 PM").X; + float sourceColWidth = ImGui.CalcTextSize("PairManager").X; + float uidColWidth = ImGui.CalcTextSize("WWWWWWW").X; + float characterColWidth = ImGui.CalcTextSize("Wwwwww Wwwwww").X; + + if (table) + { + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.NoSort); + ImGui.TableSetupColumn("Time", ImGuiTableColumnFlags.None, timeColWidth); + ImGui.TableSetupColumn("Source", ImGuiTableColumnFlags.None, sourceColWidth); + ImGui.TableSetupColumn("UID", ImGuiTableColumnFlags.None, uidColWidth); + ImGui.TableSetupColumn("Character", ImGuiTableColumnFlags.None, characterColWidth); + ImGui.TableSetupColumn("Event", ImGuiTableColumnFlags.None); + ImGui.TableHeadersRow(); + int i = 0; + foreach (var ev in _filteredEvents.Value) + { + ++i; + + var icon = ev.EventSeverity switch + { + EventSeverity.Informational => FontAwesomeIcon.InfoCircle, + EventSeverity.Warning => FontAwesomeIcon.ExclamationTriangle, + EventSeverity.Error => FontAwesomeIcon.Cross, + _ => FontAwesomeIcon.QuestionCircle + }; + + var iconColor = ev.EventSeverity switch + { + EventSeverity.Informational => new Vector4(), + EventSeverity.Warning => ImGuiColors.DalamudYellow, + EventSeverity.Error => ImGuiColors.DalamudRed, + _ => new Vector4() + }; + + ImGui.TableNextColumn(); + _uiSharedService.IconText(icon, iconColor == new Vector4() ? null : iconColor); + UiSharedService.AttachToolTip(ev.EventSeverity.ToString()); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(ev.EventTime.ToString("T", CultureInfo.CurrentCulture)); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(ev.EventSource); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + if (!string.IsNullOrEmpty(ev.UID)) + { + if (ImGui.Selectable(ev.UID + $"##{i}")) + { + _filterFreeText = ev.UID; + _filteredEvents = RecreateFilter(); + } + } + else + { + ImGui.TextUnformatted("--"); + } + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + if (!string.IsNullOrEmpty(ev.Character)) + { + if (ImGui.Selectable(ev.Character + $"##{i}")) + { + _filterFreeText = ev.Character; + _filteredEvents = RecreateFilter(); + } + } + else + { + ImGui.TextUnformatted("--"); + } + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + var posX = ImGui.GetCursorPosX(); + var maxTextLength = ImGui.GetWindowContentRegionMax().X - posX; + var textSize = ImGui.CalcTextSize(ev.Message).X; + var msg = ev.Message; + while (textSize > maxTextLength) + { + msg = msg[..^5] + "..."; + textSize = ImGui.CalcTextSize(msg).X; + } + ImGui.TextUnformatted(msg); + if (!string.Equals(msg, ev.Message, StringComparison.Ordinal)) + { + UiSharedService.AttachToolTip(ev.Message); + } + } + } + } +} diff --git a/MareSynchronos/UI/Handlers/TagHandler.cs b/MareSynchronos/UI/Handlers/TagHandler.cs new file mode 100644 index 0000000..11f2d41 --- /dev/null +++ b/MareSynchronos/UI/Handlers/TagHandler.cs @@ -0,0 +1,85 @@ +using MareSynchronos.Services.ServerConfiguration; + +namespace MareSynchronos.UI.Handlers; + +public class TagHandler +{ + public const string CustomOfflineTag = "Mare_Offline"; + public const string CustomOfflineSyncshellTag = "Mare_OfflineSyncshell"; + public const string CustomOnlineTag = "Mare_Online"; + public const string CustomUnpairedTag = "Mare_Unpaired"; + public const string CustomVisibleTag = "Mare_Visible"; + private readonly ServerConfigurationManager _serverConfigurationManager; + + public TagHandler(ServerConfigurationManager serverConfigurationManager) + { + _serverConfigurationManager = serverConfigurationManager; + } + + public void AddTag(string tag) + { + _serverConfigurationManager.AddTag(tag); + } + + public void AddTagToPairedUid(string uid, string tagName) + { + _serverConfigurationManager.AddTagForUid(uid, tagName); + } + + public List GetAllTagsSorted() + { + return + [ + .. _serverConfigurationManager.GetServerAvailablePairTags() + .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) +, + ]; + } + + public HashSet GetOtherUidsForTag(string tag) + { + return _serverConfigurationManager.GetUidsForTag(tag); + } + + public bool HasAnyTag(string uid) + { + return _serverConfigurationManager.HasTags(uid); + } + + public bool HasTag(string uid, string tagName) + { + return _serverConfigurationManager.ContainsTag(uid, tagName); + } + + /// + /// Is this tag opened in the paired clients UI? + /// + /// the tag + /// open true/false + public bool IsTagOpen(string tag) + { + return _serverConfigurationManager.ContainsOpenPairTag(tag); + } + + public void RemoveTag(string tag) + { + _serverConfigurationManager.RemoveTag(tag); + } + + public void RemoveTagFromPairedUid(string uid, string tagName) + { + _serverConfigurationManager.RemoveTagForUid(uid, tagName); + } + + public void SetTagOpen(string tag, bool open) + { + if (open) + { + _serverConfigurationManager.AddOpenPairTag(tag); + } + else + { + _serverConfigurationManager.RemoveOpenPairTag(tag); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/Handlers/UidDisplayHandler.cs b/MareSynchronos/UI/Handlers/UidDisplayHandler.cs new file mode 100644 index 0000000..e4e506e --- /dev/null +++ b/MareSynchronos/UI/Handlers/UidDisplayHandler.cs @@ -0,0 +1,204 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI.Components; + +namespace MareSynchronos.UI.Handlers; + +public class UidDisplayHandler +{ + private readonly MareConfigService _mareConfigService; + private readonly MareMediator _mediator; + private readonly PairManager _pairManager; + private readonly ServerConfigurationManager _serverManager; + private readonly Dictionary _showUidForEntry = new(StringComparer.Ordinal); + private string _editNickEntry = string.Empty; + private string _editUserComment = string.Empty; + private string _lastMouseOverUid = string.Empty; + private bool _popupShown = false; + private DateTime? _popupTime; + + public UidDisplayHandler(MareMediator mediator, PairManager pairManager, + ServerConfigurationManager serverManager, MareConfigService mareConfigService) + { + _mediator = mediator; + _pairManager = pairManager; + _serverManager = serverManager; + _mareConfigService = mareConfigService; + } + + public void RenderPairList(IEnumerable pairs) + { + var textHeight = ImGui.GetFontSize(); + var style = ImGui.GetStyle(); + var framePadding = style.FramePadding; + var spacing = style.ItemSpacing; + var lineHeight = textHeight + framePadding.Y * 2 + spacing.Y; + var startY = ImGui.GetCursorStartPos().Y; + var cursorY = ImGui.GetCursorPosY(); + var contentHeight = UiSharedService.GetWindowContentRegionHeight(); + + foreach (var entry in pairs) + { + if ((startY + cursorY) < -lineHeight || (startY + cursorY) > contentHeight) + { + cursorY += lineHeight; + ImGui.SetCursorPosY(cursorY); + continue; + } + + using (ImRaii.PushId(entry.ImGuiID)) entry.DrawPairedClient(); + cursorY += lineHeight; + } + } + + public void DrawPairText(string id, Pair pair, float textPosX, float originalY, Func editBoxWidth) + { + ImGui.SameLine(textPosX); + (bool textIsUid, string playerText) = GetPlayerText(pair); + if (!string.Equals(_editNickEntry, pair.UserData.UID, StringComparison.Ordinal)) + { + ImGui.SetCursorPosY(originalY); + + using (ImRaii.PushFont(UiBuilder.MonoFont, textIsUid)) ImGui.TextUnformatted(playerText); + + if (ImGui.IsItemHovered()) + { + if (!string.Equals(_lastMouseOverUid, id)) + { + _popupTime = DateTime.UtcNow.AddSeconds(_mareConfigService.Current.ProfileDelay); + } + + _lastMouseOverUid = id; + + if (_popupTime > DateTime.UtcNow || !_mareConfigService.Current.ProfilesShow) + { + ImGui.SetTooltip("Left click to switch between UID display and nick" + Environment.NewLine + + "Right click to change nick for " + pair.UserData.AliasOrUID + Environment.NewLine + + "Middle Mouse Button to open their profile in a separate window"); + } + else if (_popupTime < DateTime.UtcNow && !_popupShown) + { + _popupShown = true; + _mediator.Publish(new ProfilePopoutToggle(pair)); + } + } + else + { + if (string.Equals(_lastMouseOverUid, id)) + { + _mediator.Publish(new ProfilePopoutToggle(null)); + _lastMouseOverUid = string.Empty; + _popupShown = false; + } + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + var prevState = textIsUid; + if (_showUidForEntry.ContainsKey(pair.UserData.UID)) + { + prevState = _showUidForEntry[pair.UserData.UID]; + } + _showUidForEntry[pair.UserData.UID] = !prevState; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + var nickEntryPair = _pairManager.DirectPairs.Find(p => string.Equals(p.UserData.UID, _editNickEntry, StringComparison.Ordinal)); + nickEntryPair?.SetNote(_editUserComment); + _editUserComment = pair.GetNote() ?? string.Empty; + _editNickEntry = pair.UserData.UID; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Middle)) + { + _mediator.Publish(new ProfileOpenStandaloneMessage(pair)); + } + } + else + { + ImGui.SetCursorPosY(originalY); + + ImGui.SetNextItemWidth(editBoxWidth.Invoke()); + if (ImGui.InputTextWithHint("##" + pair.UserData.UID, "Nick/Notes", ref _editUserComment, 255, ImGuiInputTextFlags.EnterReturnsTrue)) + { + _serverManager.SetNoteForUid(pair.UserData.UID, _editUserComment); + _serverManager.SaveNotes(); + _editNickEntry = string.Empty; + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _editNickEntry = string.Empty; + } + UiSharedService.AttachToolTip("Hit ENTER to save\nRight click to cancel"); + } + } + + public (bool isUid, string text) GetPlayerText(Pair pair) + { + var textIsUid = true; + bool showUidInsteadOfName = ShowUidInsteadOfName(pair); + string? playerText = _serverManager.GetNoteForUid(pair.UserData.UID); + if (!showUidInsteadOfName && playerText != null) + { + if (string.IsNullOrEmpty(playerText)) + { + playerText = pair.UserData.AliasOrUID; + } + else + { + textIsUid = false; + } + } + else + { + playerText = pair.UserData.AliasOrUID; + } + + if (_mareConfigService.Current.ShowCharacterNames && textIsUid && !showUidInsteadOfName) + { + var name = pair.PlayerName; + if (name != null) + { + playerText = name; + textIsUid = false; + var note = pair.GetNote(); + if (note != null) + { + playerText = note; + } + } + } + + return (textIsUid, playerText!); + } + + internal void Clear() + { + _editNickEntry = string.Empty; + _editUserComment = string.Empty; + } + + internal void OpenProfile(Pair entry) + { + _mediator.Publish(new ProfileOpenStandaloneMessage(entry)); + } + + internal void OpenAnalysis(Pair entry) + { + _mediator.Publish(new OpenPairAnalysisWindow(entry)); + } + + private bool ShowUidInsteadOfName(Pair pair) + { + _showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName); + + return showUidInsteadOfName; + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/IntroUI.cs b/MareSynchronos/UI/IntroUI.cs new file mode 100644 index 0000000..74da657 --- /dev/null +++ b/MareSynchronos/UI/IntroUI.cs @@ -0,0 +1,380 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; +using MareSynchronos.API.Dto.Account; +using MareSynchronos.FileCache; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.WebAPI; +using MareSynchronos.WebAPI.SignalR.Utils; +using Microsoft.Extensions.Logging; +using System.Numerics; +using System.Text.RegularExpressions; + +namespace MareSynchronos.UI; + +public partial class IntroUi : WindowMediatorSubscriberBase +{ + private readonly MareConfigService _configService; + private readonly CacheMonitor _cacheMonitor; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly DalamudUtilService _dalamudUtilService; + private readonly AccountRegistrationService _registerService; + private readonly UiSharedService _uiShared; + private bool _readFirstPage; + + private string _secretKey = string.Empty; + private string _timeoutLabel = string.Empty; + private Task? _timeoutTask; + private bool _registrationInProgress = false; + private bool _registrationSuccess = false; + private string? _registrationMessage; + private RegisterReplyDto? _registrationReply; + + public IntroUi(ILogger logger, UiSharedService uiShared, MareConfigService configService, + CacheMonitor fileCacheManager, ServerConfigurationManager serverConfigurationManager, MareMediator mareMediator, + PerformanceCollectorService performanceCollectorService, DalamudUtilService dalamudUtilService, AccountRegistrationService registerService) : base(logger, mareMediator, "Snowcloak Setup", performanceCollectorService) + { + _uiShared = uiShared; + _configService = configService; + _cacheMonitor = fileCacheManager; + _serverConfigurationManager = serverConfigurationManager; + _dalamudUtilService = dalamudUtilService; + _registerService = registerService; + IsOpen = false; + ShowCloseButton = false; + RespectCloseHotkey = false; + + SizeConstraints = new WindowSizeConstraints() + { + MinimumSize = new Vector2(650, 500), + MaximumSize = new Vector2(650, 2000), + }; + + Mediator.Subscribe(this, (_) => IsOpen = false); + Mediator.Subscribe(this, (_) => + { + _configService.Current.UseCompactor = !dalamudUtilService.IsWine; + IsOpen = true; + }); + } + + private Vector4 GetConnectionColor() + { + return _uiShared.ApiController.ServerState switch + { + ServerState.Connecting => ImGuiColors.DalamudYellow, + ServerState.Reconnecting => ImGuiColors.DalamudRed, + ServerState.Connected => ImGuiColors.HealerGreen, + ServerState.Disconnected => ImGuiColors.DalamudYellow, + ServerState.Disconnecting => ImGuiColors.DalamudYellow, + ServerState.Unauthorized => ImGuiColors.DalamudRed, + ServerState.VersionMisMatch => ImGuiColors.DalamudRed, + ServerState.Offline => ImGuiColors.DalamudRed, + ServerState.RateLimited => ImGuiColors.DalamudYellow, + ServerState.NoSecretKey => ImGuiColors.DalamudYellow, + ServerState.MultiChara => ImGuiColors.DalamudYellow, + _ => ImGuiColors.DalamudRed + }; + } + + private string GetConnectionStatus() + { + return _uiShared.ApiController.ServerState 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.Connected => "Connected", + _ => string.Empty + }; + } + + protected override void DrawInternal() + { + if (_uiShared.IsInGpose) return; + + if (!_configService.Current.AcceptedAgreement && !_readFirstPage) + { + _uiShared.BigText("Welcome to Snowcloak"); + ImGui.Separator(); + UiSharedService.TextWrapped("Snowcloak is a plugin that will replicate your full current character state including all Penumbra mods to other paired users. " + + "Note that you will have to have Penumbra as well as Glamourer installed to use this plugin."); + UiSharedService.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue."); + + UiSharedService.ColorTextWrapped("Note: Any modifications you have applied through anything but Penumbra cannot be shared and your character state on other clients " + + "might look broken because of this or others players mods might not apply on your end altogether. " + + "If you want to use this plugin you will have to move your mods to Penumbra.", ImGuiColors.DalamudYellow); + if (!_uiShared.DrawOtherPluginState(intro: true)) return; + ImGui.Separator(); + if (ImGui.Button("Next##toAgreement")) + { + _readFirstPage = true; +#if !DEBUG + _timeoutTask = Task.Run(async () => + { + for (int i = 10; i > 0; i--) + { + _timeoutLabel = $"'I agree' button will be available in {i}s"; + await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); + } + }); +#else + _timeoutTask = Task.CompletedTask; +#endif + } + } + else if (!_configService.Current.AcceptedAgreement && _readFirstPage) + { + using (_uiShared.UidFont.Push()) + { + ImGui.TextUnformatted("Agreement of Usage of Service"); + } + + ImGui.Separator(); + ImGui.SetWindowFontScale(1.5f); + string readThis = "READ THIS CAREFULLY"; + Vector2 textSize = ImGui.CalcTextSize(readThis); + ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2); + UiSharedService.ColorText(readThis, ImGuiColors.DalamudRed); + ImGui.SetWindowFontScale(1.0f); + ImGui.Separator(); + UiSharedService.TextWrapped(""" + To use Snowcloak, you must be over the age of 18, or 21 in some jurisdictions. + """); + UiSharedService.TextWrapped(""" + All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. The plugin will exclusively upload the necessary mod files and not the whole mod. + """); + UiSharedService.TextWrapped(""" + If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. Files present on the service that already represent your active mod files will not be uploaded again. + """); + UiSharedService.TextWrapped(""" + The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod. + """); + UiSharedService.TextWrapped(""" + The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone. + """); + UiSharedService.TextWrapped(""" + Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted. + """); + UiSharedService.TextWrapped(""" + Accounts that are inactive for ninety (90) days will be deleted for privacy reasons. + """); + UiSharedService.TextWrapped(""" + Snowcloak is operated from servers located in the European Union. You agree not to upload any content to the service that violates EU law; and more specifically, German law. + """); + UiSharedService.TextWrapped(""" + You may delete your account at any time from within the Settings panel of the plugin. Any mods unique to you will then be removed from the server within 14 days. + """); + UiSharedService.TextWrapped(""" + This service is provided as-is. + """); + + ImGui.Separator(); + if (_timeoutTask?.IsCompleted ?? true) + { + if (ImGui.Button("I agree##toSetup")) + { + _configService.Current.AcceptedAgreement = true; + _configService.Save(); + } + } + else + { + UiSharedService.TextWrapped(_timeoutLabel); + } + } + else if (_configService.Current.AcceptedAgreement + && (string.IsNullOrEmpty(_configService.Current.CacheFolder) + || !_configService.Current.InitialScanComplete + || !Directory.Exists(_configService.Current.CacheFolder))) + { + using (_uiShared.UidFont.Push()) + ImGui.TextUnformatted("File Storage Setup"); + + ImGui.Separator(); + + if (!_uiShared.HasValidPenumbraModPath) + { + UiSharedService.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", ImGuiColors.DalamudRed); + } + else + { + UiSharedService.TextWrapped("To not unnecessary download files already present on your computer, Snowcloak will have to scan your Penumbra mod directory. " + + "Additionally, a local storage folder must be set where Snowcloak will download other character files to. " + + "Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service."); + UiSharedService.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed."); + UiSharedService.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.csv of Snowcloak in the Plugin Configurations folder of Dalamud. " + + "Otherwise on the next launch a full re-scan of the file cache database will be initiated.", ImGuiColors.DalamudYellow); + UiSharedService.ColorTextWrapped("Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly.", ImGuiColors.DalamudYellow); + _uiShared.DrawCacheDirectorySetting(); + } + + if (!_cacheMonitor.IsScanRunning && !string.IsNullOrEmpty(_configService.Current.CacheFolder) && _uiShared.HasValidPenumbraModPath && Directory.Exists(_configService.Current.CacheFolder)) + { + if (ImGui.Button("Start Scan##startScan")) + { + _cacheMonitor.InvokeScan(); + } + } + else + { + _uiShared.DrawFileScanState(); + } + if (!_dalamudUtilService.IsWine) + { + var useFileCompactor = _configService.Current.UseCompactor; + if (ImGui.Checkbox("Use File Compactor", ref useFileCompactor)) + { + _configService.Current.UseCompactor = useFileCompactor; + _configService.Save(); + } + UiSharedService.ColorTextWrapped("The File Compactor can save a tremendeous amount of space on the hard disk for downloads through Snowcloak. It will incur a minor CPU penalty on download but can speed up " + + "loading of other characters. It is recommended to keep it enabled. You can change this setting later anytime in the Snowcloak settings.", ImGuiColors.DalamudYellow); + } + } + else if (!_uiShared.ApiController.IsConnected) + { + using (_uiShared.UidFont.Push()) + ImGui.TextUnformatted("Service Registration"); + ImGui.Separator(); + UiSharedService.TextWrapped("To be able to use Snowcloak you will have to register an account."); + UiSharedService.TextWrapped("Refer to the instructions at the location you obtained this plugin for more information or support."); + + ImGui.Separator(); + + ImGui.BeginDisabled(_registrationInProgress || _uiShared.ApiController.ServerState == ServerState.Connecting || _uiShared.ApiController.ServerState == ServerState.Reconnecting); + _ = _uiShared.DrawServiceSelection(selectOnChange: true, intro: true); + + if (true) // Enable registration button for all servers + { + ImGui.BeginDisabled(_registrationInProgress || _registrationSuccess || _secretKey.Length > 0); + ImGui.Separator(); + ImGui.TextUnformatted("If you have not used Snowcloak before, click below to register a new account."); + if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Register a new Snowcloak account")) + { + _registrationInProgress = true; + _ = Task.Run(async () => { + try + { + var reply = await _registerService.RegisterAccount(CancellationToken.None).ConfigureAwait(false); + if (!reply.Success) + { + _logger.LogWarning("Registration failed: {err}", reply.ErrorMessage); + _registrationMessage = reply.ErrorMessage; + if (_registrationMessage.IsNullOrEmpty()) + _registrationMessage = "An unknown error occured. Please try again later."; + return; + } + _registrationMessage = "New account registered.\nPlease keep a copy of your secret key in case you need to reset your plugins, or to use it on another PC."; + _secretKey = reply.SecretKey ?? ""; + _registrationReply = reply; + _registrationSuccess = true; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Registration failed"); + _registrationSuccess = false; + _registrationMessage = "An unknown error occured. Please try again later."; + } + finally + { + _registrationInProgress = false; + } + }); + } + ImGui.EndDisabled(); // _registrationInProgress || _registrationSuccess + if (_registrationInProgress) + { + ImGui.TextUnformatted("Sending request..."); + } + else if (!_registrationMessage.IsNullOrEmpty()) + { + if (!_registrationSuccess) + ImGui.TextColored(ImGuiColors.DalamudYellow, _registrationMessage); + else + ImGui.TextWrapped(_registrationMessage); + } + } + + ImGui.Separator(); + + var text = "Enter Secret Key"; + + if (_registrationSuccess) + { + text = "Secret Key"; + } + else + { + ImGui.TextUnformatted("If you already have a registered account, you can enter its secret key below to use it instead."); + } + + var textSize = ImGui.CalcTextSize(text); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(text); + ImGui.SameLine(); + ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - textSize.X); + ImGui.InputText("", ref _secretKey, 64); + if (_secretKey.Length > 0 && _secretKey.Length != 64) + { + UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long.", ImGuiColors.DalamudRed); + } + else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey)) + { + UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", ImGuiColors.DalamudRed); + } + else if (_secretKey.Length == 64) + { + using var saveDisabled = ImRaii.Disabled(_uiShared.ApiController.ServerState == ServerState.Connecting || _uiShared.ApiController.ServerState == ServerState.Reconnecting); + if (ImGui.Button("Save and Connect")) + { + string keyName; + if (_serverConfigurationManager.CurrentServer == null) _serverConfigurationManager.SelectServer(0); + if (_registrationReply != null && _secretKey.Equals(_registrationReply.SecretKey, StringComparison.Ordinal)) + keyName = _registrationReply.UID + $" (registered {DateTime.Now:yyyy-MM-dd})"; + else + keyName = $"Secret Key added on Setup ({DateTime.Now:yyyy-MM-dd})"; + _serverConfigurationManager.CurrentServer!.SecretKeys.Add(_serverConfigurationManager.CurrentServer.SecretKeys.Select(k => k.Key).LastOrDefault() + 1, new SecretKey() + { + FriendlyName = keyName, + Key = _secretKey, + }); + _serverConfigurationManager.AddCurrentCharacterToServer(save: false); + _ = Task.Run(() => _uiShared.ApiController.CreateConnections()); + } + } + + if (_uiShared.ApiController.ServerState != ServerState.NoSecretKey) + { + UiSharedService.ColorText(GetConnectionStatus(), GetConnectionColor()); + } + + ImGui.EndDisabled(); // _registrationInProgress + } + else + { + _secretKey = string.Empty; + _serverConfigurationManager.Save(); + Mediator.Publish(new SwitchToMainUiMessage()); + IsOpen = false; + } + } + +#pragma warning disable MA0009 + [GeneratedRegex("^([A-F0-9]{2})+")] + private static partial Regex HexRegex(); +#pragma warning restore MA0009 +} diff --git a/MareSynchronos/UI/PermissionWindowUI.cs b/MareSynchronos/UI/PermissionWindowUI.cs new file mode 100644 index 0000000..1cb3ad7 --- /dev/null +++ b/MareSynchronos/UI/PermissionWindowUI.cs @@ -0,0 +1,167 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.UI; + +public class PermissionWindowUI : WindowMediatorSubscriberBase +{ + public Pair Pair { get; init; } + + private readonly UiSharedService _uiSharedService; + private readonly ApiController _apiController; + private UserPermissions _ownPermissions; + + public PermissionWindowUI(ILogger logger, Pair pair, MareMediator mediator, UiSharedService uiSharedService, + ApiController apiController, PerformanceCollectorService performanceCollectorService) + : base(logger, mediator, "Permissions for " + pair.UserData.AliasOrUID + "###SnowcloakSyncPermissions" + pair.UserData.UID, performanceCollectorService) + { + Pair = pair; + _uiSharedService = uiSharedService; + _apiController = apiController; + _ownPermissions = pair.UserPair?.OwnPermissions.DeepClone() ?? default; + Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize; + SizeConstraints = new() + { + MinimumSize = new(450, 100), + MaximumSize = new(450, 500) + }; + IsOpen = true; + } + + protected override void DrawInternal() + { + var paused = _ownPermissions.IsPaused(); + var disableSounds = _ownPermissions.IsDisableSounds(); + var disableAnimations = _ownPermissions.IsDisableAnimations(); + var disableVfx = _ownPermissions.IsDisableVFX(); + var style = ImGui.GetStyle(); + var indentSize = ImGui.GetFrameHeight() + style.ItemSpacing.X; + + _uiSharedService.BigText("Permissions for " + Pair.UserData.AliasOrUID); + ImGuiHelpers.ScaledDummy(1f); + + if (Pair.UserPair == null) + return; + + if (ImGui.Checkbox("Pause Sync", ref paused)) + { + _ownPermissions.SetPaused(paused); + } + _uiSharedService.DrawHelpText("Pausing will completely cease any sync with this user." + UiSharedService.TooltipSeparator + + "Note: this is bidirectional, either user pausing will cease sync completely."); + var otherPerms = Pair.UserPair.OtherPermissions; + + var otherIsPaused = otherPerms.IsPaused(); + var otherDisableSounds = otherPerms.IsDisableSounds(); + var otherDisableAnimations = otherPerms.IsDisableAnimations(); + var otherDisableVFX = otherPerms.IsDisableVFX(); + + using (ImRaii.PushIndent(indentSize, false)) + { + _uiSharedService.BooleanToColoredIcon(!otherIsPaused, false); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(Pair.UserData.AliasOrUID + " has " + (!otherIsPaused ? "not " : string.Empty) + "paused you"); + } + + ImGuiHelpers.ScaledDummy(0.5f); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(0.5f); + + if (ImGui.Checkbox("Disable Sounds", ref disableSounds)) + { + _ownPermissions.SetDisableSounds(disableSounds); + } + _uiSharedService.DrawHelpText("Disabling sounds will remove all sounds synced with this user on both sides." + UiSharedService.TooltipSeparator + + "Note: this is bidirectional, either user disabling sound sync will stop sound sync on both sides."); + using (ImRaii.PushIndent(indentSize, false)) + { + _uiSharedService.BooleanToColoredIcon(!otherDisableSounds, false); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(Pair.UserData.AliasOrUID + " has " + (!otherDisableSounds ? "not " : string.Empty) + "disabled sound sync with you"); + } + + if (ImGui.Checkbox("Disable Animations", ref disableAnimations)) + { + _ownPermissions.SetDisableAnimations(disableAnimations); + } + _uiSharedService.DrawHelpText("Disabling sounds will remove all animations synced with this user on both sides." + UiSharedService.TooltipSeparator + + "Note: this is bidirectional, either user disabling animation sync will stop animation sync on both sides."); + using (ImRaii.PushIndent(indentSize, false)) + { + _uiSharedService.BooleanToColoredIcon(!otherDisableAnimations, false); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(Pair.UserData.AliasOrUID + " has " + (!otherDisableAnimations ? "not " : string.Empty) + "disabled animation sync with you"); + } + + if (ImGui.Checkbox("Disable VFX", ref disableVfx)) + { + _ownPermissions.SetDisableVFX(disableVfx); + } + _uiSharedService.DrawHelpText("Disabling sounds will remove all VFX synced with this user on both sides." + UiSharedService.TooltipSeparator + + "Note: this is bidirectional, either user disabling VFX sync will stop VFX sync on both sides."); + using (ImRaii.PushIndent(indentSize, false)) + { + _uiSharedService.BooleanToColoredIcon(!otherDisableVFX, false); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(Pair.UserData.AliasOrUID + " has " + (!otherDisableVFX ? "not " : string.Empty) + "disabled VFX sync with you"); + } + + ImGuiHelpers.ScaledDummy(0.5f); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(0.5f); + + bool hasChanges = _ownPermissions != Pair.UserPair.OwnPermissions; + + using (ImRaii.Disabled(!hasChanges)) + if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Save, "Save")) + { + _ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions)); + } + UiSharedService.AttachToolTip("Save and apply all changes"); + + var rightSideButtons = _uiSharedService.GetIconTextButtonSize(Dalamud.Interface.FontAwesomeIcon.Undo, "Revert") + + _uiSharedService.GetIconTextButtonSize(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default"); + var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + + ImGui.SameLine(availableWidth - rightSideButtons); + + using (ImRaii.Disabled(!hasChanges)) + if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Undo, "Revert")) + { + _ownPermissions = Pair.UserPair.OwnPermissions.DeepClone(); + } + UiSharedService.AttachToolTip("Revert all changes"); + + ImGui.SameLine(); + if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default")) + { + _ownPermissions.SetPaused(false); + _ownPermissions.SetDisableVFX(false); + _ownPermissions.SetDisableSounds(false); + _ownPermissions.SetDisableAnimations(false); + _ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions)); + } + UiSharedService.AttachToolTip("This will set all permissions to their default setting"); + + var ySize = ImGui.GetCursorPosY() + style.FramePadding.Y * ImGuiHelpers.GlobalScale + style.FrameBorderSize; + ImGui.SetWindowSize(new(400, ySize)); + } + + public override void OnClose() + { + Mediator.Publish(new RemoveWindowMessage(this)); + } +} diff --git a/MareSynchronos/UI/PlayerAnalysisUI.cs b/MareSynchronos/UI/PlayerAnalysisUI.cs new file mode 100644 index 0000000..2c1bc3d --- /dev/null +++ b/MareSynchronos/UI/PlayerAnalysisUI.cs @@ -0,0 +1,366 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using Microsoft.Extensions.Logging; +using System.Numerics; + +namespace MareSynchronos.UI; + +public class PlayerAnalysisUI : WindowMediatorSubscriberBase +{ + private readonly UiSharedService _uiSharedService; + private Dictionary>? _cachedAnalysis; + private bool _hasUpdate = true; + private bool _sortDirty = true; + private string _selectedFileTypeTab = string.Empty; + private string _selectedHash = string.Empty; + private ObjectKind _selectedObjectTab; + + public PlayerAnalysisUI(ILogger logger, Pair pair, MareMediator mediator, UiSharedService uiSharedService, + PerformanceCollectorService performanceCollectorService) + : base(logger, mediator, "Character Data Analysis for " + pair.UserData.AliasOrUID + "###SnowcloakPairAnalysis" + pair.UserData.UID, performanceCollectorService) + { + Pair = pair; + _uiSharedService = uiSharedService; + Mediator.SubscribeKeyed(this, Pair.UserData.UID, (_) => + { + _logger.LogInformation("PairDataAnalyzedMessage received for {uid}", Pair.UserData.UID); + _hasUpdate = true; + }); + SizeConstraints = new() + { + MinimumSize = new() + { + X = 800, + Y = 600 + }, + MaximumSize = new() + { + X = 3840, + Y = 2160 + } + }; + IsOpen = true; + } + + public Pair Pair { get; private init; } + public PairAnalyzer? PairAnalyzer => Pair.PairAnalyzer; + + public override void OnClose() + { + Mediator.Publish(new RemoveWindowMessage(this)); + } + + protected override void DrawInternal() + { + if (PairAnalyzer == null) return; + PairAnalyzer analyzer = PairAnalyzer!; + + if (_hasUpdate) + { + _cachedAnalysis = analyzer.LastAnalysis.DeepClone(); + _hasUpdate = false; + _sortDirty = true; + } + + UiSharedService.TextWrapped($"This window shows you all files and their sizes that are currently in use by {Pair.UserData.AliasOrUID} and associated entities"); + + if (_cachedAnalysis == null || _cachedAnalysis.Count == 0) return; + + bool isAnalyzing = analyzer.IsAnalysisRunning; + bool needAnalysis = _cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed)); + if (isAnalyzing) + { + UiSharedService.ColorTextWrapped($"Analyzing {analyzer.CurrentFile}/{analyzer.TotalFiles}", + ImGuiColors.DalamudYellow); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis")) + { + analyzer.CancelAnalyze(); + } + } + else + { + if (needAnalysis) + { + UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to compute missing data", + ImGuiColors.DalamudYellow); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)")) + { + _ = analyzer.ComputeAnalysis(print: false); + } + } + } + + 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(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis)) + { + ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize)))); + if (needAnalysis && !isAnalyzing) + { + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TextUnformatted(FontAwesomeIcon.ExclamationCircle.ToIconString()); + UiSharedService.AttachToolTip("Click \"Start analysis\" to calculate download size"); + } + } + ImGui.TextUnformatted($"Total modded model triangles: {UiSharedService.TrisToString(_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles)))}"); + ImGui.Separator(); + + var playerName = analyzer.LastPlayerName; + + if (playerName.Length == 0) + { + playerName = Pair.PlayerName ?? string.Empty; + analyzer.LastPlayerName = playerName; + } + + using var tabbar = ImRaii.TabBar("objectSelection"); + foreach (var kvp in _cachedAnalysis) + { + using var id = ImRaii.PushId(kvp.Key.ToString()); + string tabText = kvp.Key == ObjectKind.Player ? playerName : $"{playerName}'s {kvp.Key}"; + using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString()); + if (tab.Success) + { + var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal) + .OrderBy(k => k.Key, StringComparer.Ordinal).ToList(); + + ImGui.TextUnformatted($"Files for {tabText}"); + + ImGui.SameLine(); + 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 = ""; + text = string.Join(Environment.NewLine, groupedfiles + .Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize)) + + ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize)))); + ImGui.SetTooltip(text); + } + ImGui.TextUnformatted($"{kvp.Key} size (actual):"); + ImGui.SameLine(); + ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize))); + ImGui.TextUnformatted($"{kvp.Key} size (download size):"); + ImGui.SameLine(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, needAnalysis)) + { + ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize))); + if (needAnalysis && !isAnalyzing) + { + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TextUnformatted(FontAwesomeIcon.ExclamationCircle.ToIconString()); + UiSharedService.AttachToolTip("Click \"Start analysis\" to calculate download size"); + } + } + ImGui.TextUnformatted($"{kvp.Key} VRAM usage:"); + ImGui.SameLine(); + var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal)); + if (vramUsage != null) + { + ImGui.TextUnformatted(UiSharedService.ByteToString(vramUsage.Sum(f => f.OriginalSize))); + } + ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {UiSharedService.TrisToString(kvp.Value.Sum(f => f.Value.Triangles))}"); + + ImGui.Separator(); + if (_selectedObjectTab != kvp.Key) + { + _selectedHash = string.Empty; + _selectedObjectTab = kvp.Key; + _selectedFileTypeTab = string.Empty; + } + + using var fileTabBar = ImRaii.TabBar("fileTabs"); + + foreach (IGrouping? fileGroup in groupedfiles) + { + string fileGroupText = fileGroup.Key + " [" + fileGroup.Count() + "]"; + var requiresCompute = fileGroup.Any(k => !k.IsComputed); + using var tabcol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(ImGuiColors.DalamudYellow), requiresCompute); + 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; + } + + 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 (download size):"); + ImGui.SameLine(); + ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.CompressedSize))); + + ImGui.Separator(); + DrawTable(fileGroup); + + fileTab.Dispose(); + } + } + } + + ImGui.Separator(); + + ImGui.TextUnformatted("Selected file:"); + ImGui.SameLine(); + UiSharedService.ColorText(_selectedHash, ImGuiColors.DalamudYellow); + + if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item)) + { + var gamepaths = item.GamePaths; + ImGui.TextUnformatted("Used by game path:"); + ImGui.SameLine(); + UiSharedService.TextWrapped(gamepaths[0]); + if (gamepaths.Count > 1) + { + ImGui.SameLine(); + ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)"); + ImGui.SameLine(); + _uiSharedService.IconText(FontAwesomeIcon.InfoCircle); + UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1))); + } + } + } + + private void DrawTable(IGrouping fileGroup) + { + var tableColumns = string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) + ? 5 + : (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) ? 5 : 4); + using var table = ImRaii.Table("Analysis", tableColumns, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, + new Vector2(0, 300)); + if (!table.Success) return; + ImGui.TableSetupColumn("Hash"); + ImGui.TableSetupColumn("Gamepaths", ImGuiTableColumnFlags.PreferSortDescending); + ImGui.TableSetupColumn("File Size", ImGuiTableColumnFlags.DefaultSort | ImGuiTableColumnFlags.PreferSortDescending); + ImGui.TableSetupColumn("Download Size", ImGuiTableColumnFlags.PreferSortDescending); + if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal)) + { + ImGui.TableSetupColumn("Format"); + } + if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal)) + { + ImGui.TableSetupColumn("Triangles", ImGuiTableColumnFlags.PreferSortDescending); + } + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + + var sortSpecs = ImGui.TableGetSortSpecs(); + if (sortSpecs.SpecsDirty || _sortDirty) + { + var idx = sortSpecs.Specs.ColumnIndex; + + 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.GamePaths.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.GamePaths.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.OriginalSize).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.OriginalSize).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.CompressedSize).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.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal); + if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) && idx == 4 && 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 == 4 && 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 == 4 && 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 == 4 && 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); + + sortSpecs.SpecsDirty = false; + _sortDirty = 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); + ImGui.TableNextColumn(); + if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal)) + { + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudYellow)); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudYellow)); + } + ImGui.TextUnformatted(item.Hash); + 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(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, !item.IsComputed)) + 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 (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal)) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.TrisToString(item.Triangles)); + if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + } + } + } +} diff --git a/MareSynchronos/UI/PopoutProfileUi.cs b/MareSynchronos/UI/PopoutProfileUi.cs new file mode 100644 index 0000000..2c7538f --- /dev/null +++ b/MareSynchronos/UI/PopoutProfileUi.cs @@ -0,0 +1,185 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Interface.Utility; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.MareConfiguration; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using Microsoft.Extensions.Logging; +using System.Numerics; + +namespace MareSynchronos.UI; + +public class PopoutProfileUi : WindowMediatorSubscriberBase +{ + private readonly MareProfileManager _mareProfileManager; + private readonly PairManager _pairManager; + private readonly ServerConfigurationManager _serverManager; + private readonly UiSharedService _uiSharedService; + private Vector2 _lastMainPos = Vector2.Zero; + private Vector2 _lastMainSize = Vector2.Zero; + private byte[] _lastProfilePicture = []; + private byte[] _lastSupporterPicture = []; + private Pair? _pair; + private IDalamudTextureWrap? _supporterTextureWrap; + private IDalamudTextureWrap? _textureWrap; + + public PopoutProfileUi(ILogger logger, MareMediator mediator, UiSharedService uiSharedService, + ServerConfigurationManager serverManager, MareConfigService mareConfigService, + MareProfileManager mareProfileManager, PairManager pairManager, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "###SnowcloakSyncPopoutProfileUI", performanceCollectorService) + { + _uiSharedService = uiSharedService; + _serverManager = serverManager; + _mareProfileManager = mareProfileManager; + _pairManager = pairManager; + Flags = ImGuiWindowFlags.NoDecoration; + + Mediator.Subscribe(this, (msg) => + { + IsOpen = msg.Pair != null; + _pair = msg.Pair; + _lastProfilePicture = []; + _lastSupporterPicture = []; + _textureWrap?.Dispose(); + _textureWrap = null; + _supporterTextureWrap?.Dispose(); + _supporterTextureWrap = null; + }); + + Mediator.Subscribe(this, (msg) => + { + if (msg.Size != Vector2.Zero) + { + var border = ImGui.GetStyle().WindowBorderSize; + var padding = ImGui.GetStyle().WindowPadding; + Size = new(256 + (padding.X * 2) + border, msg.Size.Y / ImGuiHelpers.GlobalScale); + _lastMainSize = msg.Size; + } + var mainPos = msg.Position == Vector2.Zero ? _lastMainPos : msg.Position; + if (mareConfigService.Current.ProfilePopoutRight) + { + Position = new(mainPos.X + _lastMainSize.X * ImGuiHelpers.GlobalScale, mainPos.Y); + } + else + { + Position = new(mainPos.X - Size!.Value.X * ImGuiHelpers.GlobalScale, mainPos.Y); + } + + if (msg.Position != Vector2.Zero) + { + _lastMainPos = msg.Position; + } + }); + + IsOpen = false; + } + + protected override void DrawInternal() + { + if (_pair == null) return; + + try + { + var spacing = ImGui.GetStyle().ItemSpacing; + + var mareProfile = _mareProfileManager.GetMareProfile(_pair.UserData); + + if (_textureWrap == null || !mareProfile.ImageData.Value.SequenceEqual(_lastProfilePicture)) + { + _textureWrap?.Dispose(); + _lastProfilePicture = mareProfile.ImageData.Value; + + _textureWrap = _uiSharedService.LoadImage(_lastProfilePicture); + } + + var drawList = ImGui.GetWindowDrawList(); + var rectMin = drawList.GetClipRectMin(); + var rectMax = drawList.GetClipRectMax(); + + using (_uiSharedService.UidFont.Push()) + UiSharedService.ColorText(_pair.UserData.AliasOrUID, UiSharedService.AccentColor); + + ImGuiHelpers.ScaledDummy(spacing.Y, spacing.Y); + var textPos = ImGui.GetCursorPosY(); + ImGui.Separator(); + var imagePos = ImGui.GetCursorPos(); + ImGuiHelpers.ScaledDummy(256, 256 * ImGuiHelpers.GlobalScale + spacing.Y); + var note = _serverManager.GetNoteForUid(_pair.UserData.UID); + if (!string.IsNullOrEmpty(note)) + { + UiSharedService.ColorText(note, ImGuiColors.DalamudGrey); + } + string status = _pair.IsVisible ? "Visible" : (_pair.IsOnline ? "Online" : "Offline"); + UiSharedService.ColorText(status, (_pair.IsVisible || _pair.IsOnline) ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed); + if (_pair.IsVisible) + { + ImGui.SameLine(); + ImGui.TextUnformatted($"({_pair.PlayerName})"); + } + if (_pair.UserPair != null) + { + ImGui.TextUnformatted("Directly paired"); + if (_pair.UserPair.OwnPermissions.IsPaused()) + { + ImGui.SameLine(); + UiSharedService.ColorText("You: paused", ImGuiColors.DalamudYellow); + } + if (_pair.UserPair.OtherPermissions.IsPaused()) + { + ImGui.SameLine(); + UiSharedService.ColorText("They: paused", ImGuiColors.DalamudYellow); + } + } + if (_pair.GroupPair.Any()) + { + ImGui.TextUnformatted("Paired through Syncshells:"); + foreach (var groupPair in _pair.GroupPair.Select(k => k.Key)) + { + var groupNote = _serverManager.GetNoteForGid(groupPair.GID); + var groupName = groupPair.GroupAliasOrGID; + var groupString = string.IsNullOrEmpty(groupNote) ? groupName : $"{groupNote} ({groupName})"; + ImGui.TextUnformatted("- " + groupString); + } + } + + ImGui.Separator(); + _uiSharedService.GameFont.Push(); + var remaining = ImGui.GetWindowContentRegionMax().Y - ImGui.GetCursorPosY(); + var descText = mareProfile.Description; + var textSize = ImGui.CalcTextSize(descText, hideTextAfterDoubleHash: false, 256f * ImGuiHelpers.GlobalScale); + bool trimmed = textSize.Y > remaining; + while (textSize.Y > remaining && descText.Contains(' ')) + { + descText = descText[..descText.LastIndexOf(' ')].TrimEnd(); + textSize = ImGui.CalcTextSize(descText + $"...{Environment.NewLine}[Open Full Profile for complete description]", hideTextAfterDoubleHash: false, 256f * ImGuiHelpers.GlobalScale); + } + UiSharedService.TextWrapped(trimmed ? descText + $"...{Environment.NewLine}[Open Full Profile for complete description]" : mareProfile.Description); + + _uiSharedService.GameFont.Pop(); + + 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 + imagePos.Y + remainingHeight), + new Vector2(rectMin.X + padding + remainingWidth + newWidth, rectMin.Y + spacing.Y + imagePos.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))); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during draw tooltip"); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs new file mode 100644 index 0000000..a6ae0bb --- /dev/null +++ b/MareSynchronos/UI/SettingsUi.cs @@ -0,0 +1,1922 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Game.Text; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Comparer; +using MareSynchronos.FileCache; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.WebAPI; +using MareSynchronos.WebAPI.Files; +using MareSynchronos.WebAPI.Files.Models; +using MareSynchronos.WebAPI.SignalR.Utils; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace MareSynchronos.UI; + +public class SettingsUi : WindowMediatorSubscriberBase +{ + private readonly ApiController _apiController; + private readonly IpcManager _ipcManager; + private readonly IpcProvider _ipcProvider; + private readonly CacheMonitor _cacheMonitor; + private readonly DalamudUtilService _dalamudUtilService; + private readonly MareConfigService _configService; + private readonly ConcurrentDictionary> _currentDownloads = new(); + private readonly FileCompactor _fileCompactor; + private readonly FileUploadManager _fileTransferManager; + private readonly FileTransferOrchestrator _fileTransferOrchestrator; + private readonly FileCacheManager _fileCacheManager; + private readonly PairManager _pairManager; + private readonly ChatService _chatService; + private readonly GuiHookService _guiHookService; + private readonly PerformanceCollectorService _performanceCollector; + private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; + private readonly PlayerPerformanceService _playerPerformanceService; + private readonly AccountRegistrationService _registerService; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly UiSharedService _uiShared; + private bool _deleteAccountPopupModalShown = false; + private string _lastTab = string.Empty; + private bool? _notesSuccessfullyApplied = null; + private bool _overwriteExistingLabels = false; + private bool _readClearCache = false; + private CancellationTokenSource? _validationCts; + private Task>? _validationTask; + private bool _wasOpen = false; + private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress; + private (int, int, FileCacheEntity) _currentProgress; + + private bool _registrationInProgress = false; + private bool _registrationSuccess = false; + private string? _registrationMessage; + + public SettingsUi(ILogger logger, + UiSharedService uiShared, MareConfigService configService, + PairManager pairManager, ChatService chatService, GuiHookService guiHookService, + ServerConfigurationManager serverConfigurationManager, + PlayerPerformanceConfigService playerPerformanceConfigService, PlayerPerformanceService playerPerformanceService, + MareMediator mediator, PerformanceCollectorService performanceCollector, + FileUploadManager fileTransferManager, + FileTransferOrchestrator fileTransferOrchestrator, + FileCacheManager fileCacheManager, + FileCompactor fileCompactor, ApiController apiController, + IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor, + DalamudUtilService dalamudUtilService, AccountRegistrationService registerService) : base(logger, mediator, "Snowcloak Settings", performanceCollector) + { + _configService = configService; + _pairManager = pairManager; + _chatService = chatService; + _guiHookService = guiHookService; + _serverConfigurationManager = serverConfigurationManager; + _playerPerformanceConfigService = playerPerformanceConfigService; + _playerPerformanceService = playerPerformanceService; + _performanceCollector = performanceCollector; + _fileTransferManager = fileTransferManager; + _fileTransferOrchestrator = fileTransferOrchestrator; + _fileCacheManager = fileCacheManager; + _apiController = apiController; + _ipcManager = ipcManager; + _ipcProvider = ipcProvider; + _cacheMonitor = cacheMonitor; + _dalamudUtilService = dalamudUtilService; + _registerService = registerService; + _fileCompactor = fileCompactor; + _uiShared = uiShared; + AllowClickthrough = false; + AllowPinning = false; + _validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v); + + SizeConstraints = new WindowSizeConstraints() + { + MinimumSize = new Vector2(600, 400), + MaximumSize = new Vector2(600, 2000), + }; + + Mediator.Subscribe(this, (_) => Toggle()); + Mediator.Subscribe(this, (_) => IsOpen = false); + Mediator.Subscribe(this, (_) => UiSharedService_GposeStart()); + Mediator.Subscribe(this, (_) => UiSharedService_GposeEnd()); + Mediator.Subscribe(this, (msg) => LastCreatedCharacterData = msg.CharacterData); + Mediator.Subscribe(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); + Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); + } + + public CharacterData? LastCreatedCharacterData { private get; set; } + private ApiController ApiController => _uiShared.ApiController; + + protected override void DrawInternal() + { + _ = _uiShared.DrawOtherPluginState(); + + DrawSettingsContent(); + } + + public override void OnClose() + { + _uiShared.EditTrackerPosition = false; + + base.OnClose(); + } + + private void DrawBlockedTransfers() + { + _lastTab = "BlockedTransfers"; + UiSharedService.ColorTextWrapped("Files that you attempted to upload or download that were forbidden to be transferred by their creators will appear here. " + + "If you see file paths from your drive here, then those files were not allowed to be uploaded. If you see hashes, those files were not allowed to be downloaded. " + + "Ask your paired friend to send you the mod in question through other means or acquire the mod yourself.", + ImGuiColors.DalamudGrey); + + if (ImGui.BeginTable("TransfersTable", 2, ImGuiTableFlags.SizingStretchProp)) + { + ImGui.TableSetupColumn( + $"Hash/Filename"); + ImGui.TableSetupColumn($"Forbidden by"); + + ImGui.TableHeadersRow(); + + foreach (var item in _fileTransferOrchestrator.ForbiddenTransfers) + { + ImGui.TableNextColumn(); + if (item is UploadFileTransfer transfer) + { + ImGui.TextUnformatted(transfer.LocalFile); + } + else + { + ImGui.TextUnformatted(item.Hash); + } + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.ForbiddenBy); + } + ImGui.EndTable(); + } + } + + private void DrawCurrentTransfers() + { + _lastTab = "Transfers"; + _uiShared.BigText("Transfer Settings"); + + int maxParallelDownloads = _configService.Current.ParallelDownloads; + int downloadSpeedLimit = _configService.Current.DownloadSpeedLimitInBytes; + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Global Download Speed Limit"); + ImGui.SameLine(); + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("###speedlimit", ref downloadSpeedLimit)) + { + _configService.Current.DownloadSpeedLimitInBytes = downloadSpeedLimit; + _configService.Save(); + Mediator.Publish(new DownloadLimitChangedMessage()); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("###speed", [DownloadSpeeds.Bps, DownloadSpeeds.KBps, DownloadSpeeds.MBps], + (s) => s switch + { + DownloadSpeeds.Bps => "Byte/s", + DownloadSpeeds.KBps => "KB/s", + DownloadSpeeds.MBps => "MB/s", + _ => throw new NotSupportedException() + }, (s) => + { + _configService.Current.DownloadSpeedType = s; + _configService.Save(); + Mediator.Publish(new DownloadLimitChangedMessage()); + }, _configService.Current.DownloadSpeedType); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("0 = No limit/infinite"); + + ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); + if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10)) + { + _configService.Current.ParallelDownloads = maxParallelDownloads; + _configService.Save(); + } + + ImGui.Separator(); + _uiShared.BigText("Transfer UI"); + + bool showTransferWindow = _configService.Current.ShowTransferWindow; + if (ImGui.Checkbox("Show separate transfer window", ref showTransferWindow)) + { + _configService.Current.ShowTransferWindow = showTransferWindow; + _configService.Save(); + } + _uiShared.DrawHelpText($"The download window will show the current progress of outstanding downloads.{Environment.NewLine}{Environment.NewLine}" + + $"What do W/Q/P/D stand for?{Environment.NewLine}W = Waiting for Slot (see Maximum Parallel Downloads){Environment.NewLine}" + + $"Q = Queued on Server, waiting for queue ready signal{Environment.NewLine}" + + $"P = Processing download (aka downloading){Environment.NewLine}" + + $"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; + } + ImGui.Unindent(); + if (!_configService.Current.ShowTransferWindow) ImGui.EndDisabled(); + + bool showTransferBars = _configService.Current.ShowTransferBars; + if (ImGui.Checkbox("Show transfer bars rendered below players", ref showTransferBars)) + { + _configService.Current.ShowTransferBars = showTransferBars; + _configService.Save(); + } + _uiShared.DrawHelpText("This will render a progress bar during the download at the feet of the player you are downloading from."); + + if (!showTransferBars) ImGui.BeginDisabled(); + ImGui.Indent(); + bool transferBarShowText = _configService.Current.TransferBarsShowText; + if (ImGui.Checkbox("Show Download Text", ref transferBarShowText)) + { + _configService.Current.TransferBarsShowText = transferBarShowText; + _configService.Save(); + } + _uiShared.DrawHelpText("Shows download text (amount of MiB downloaded) in the transfer bars"); + int transferBarWidth = _configService.Current.TransferBarsWidth; + ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); + if (ImGui.SliderInt("Transfer Bar Width", ref transferBarWidth, 0, 500)) + { + if (transferBarWidth < 10) + transferBarWidth = 10; + _configService.Current.TransferBarsWidth = transferBarWidth; + _configService.Save(); + } + _uiShared.DrawHelpText("Width of the displayed transfer bars (will never be less wide than the displayed text)"); + int transferBarHeight = _configService.Current.TransferBarsHeight; + ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); + if (ImGui.SliderInt("Transfer Bar Height", ref transferBarHeight, 0, 50)) + { + if (transferBarHeight < 2) + transferBarHeight = 2; + _configService.Current.TransferBarsHeight = transferBarHeight; + _configService.Save(); + } + _uiShared.DrawHelpText("Height of the displayed transfer bars (will never be less tall than the displayed text)"); + bool showUploading = _configService.Current.ShowUploading; + if (ImGui.Checkbox("Show 'Uploading' text below players that are currently uploading", ref showUploading)) + { + _configService.Current.ShowUploading = showUploading; + _configService.Save(); + } + _uiShared.DrawHelpText("This will render an 'Uploading' text at the feet of the player that is in progress of uploading data."); + + ImGui.Unindent(); + if (!showUploading) ImGui.BeginDisabled(); + ImGui.Indent(); + bool showUploadingBigText = _configService.Current.ShowUploadingBigText; + if (ImGui.Checkbox("Large font for 'Uploading' text", ref showUploadingBigText)) + { + _configService.Current.ShowUploadingBigText = showUploadingBigText; + _configService.Save(); + } + _uiShared.DrawHelpText("This will render an 'Uploading' text in a larger font."); + + ImGui.Unindent(); + + if (!showUploading) ImGui.EndDisabled(); + if (!showTransferBars) ImGui.EndDisabled(); + + ImGui.Separator(); + _uiShared.BigText("Current Transfers"); + + if (ImGui.BeginTabBar("TransfersTabBar")) + { + if (ApiController.ServerState is ServerState.Connected && ImGui.BeginTabItem("Transfers")) + { + ImGui.TextUnformatted("Uploads"); + if (ImGui.BeginTable("UploadsTable", 3)) + { + ImGui.TableSetupColumn("File"); + ImGui.TableSetupColumn("Uploaded"); + ImGui.TableSetupColumn("Size"); + ImGui.TableHeadersRow(); + foreach (var transfer in _fileTransferManager.CurrentUploads.ToArray()) + { + var color = UiSharedService.UploadColor((transfer.Transferred, transfer.Total)); + var col = ImRaii.PushColor(ImGuiCol.Text, color); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(transfer.Hash); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Transferred)); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(UiSharedService.ByteToString(transfer.Total)); + col.Dispose(); + ImGui.TableNextRow(); + } + + 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()) + { + 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.EndTabItem(); + } + + if (ImGui.BeginTabItem("Blocked Transfers")) + { + DrawBlockedTransfers(); + ImGui.EndTabItem(); + } + + ImGui.EndTabBar(); + } + } + + private static readonly List<(XivChatType, string)> _syncshellChatTypes = [ + (XivChatType.None, "(use global setting)"), + (XivChatType.Debug, "Debug"), + (XivChatType.Echo, "Echo"), + (XivChatType.StandardEmote, "Standard Emote"), + (XivChatType.CustomEmote, "Custom Emote"), + (XivChatType.SystemMessage, "System Message"), + (XivChatType.SystemError, "System Error"), + (XivChatType.GatheringSystemMessage, "Gathering Message"), + (XivChatType.ErrorMessage, "Error message"), + ]; + + private void DrawChatConfig() + { + _lastTab = "Chat"; + + _uiShared.BigText("Chat Settings"); + + var disableSyncshellChat = _configService.Current.DisableSyncshellChat; + + if (ImGui.Checkbox("Disable chat globally", ref disableSyncshellChat)) + { + _configService.Current.DisableSyncshellChat = disableSyncshellChat; + _configService.Save(); + } + _uiShared.DrawHelpText("Global setting to disable chat for all syncshells."); + + using var pushDisableGlobal = ImRaii.Disabled(disableSyncshellChat); + + var uiColors = _dalamudUtilService.UiColors.Value; + int globalChatColor = _configService.Current.ChatColor; + + if (globalChatColor != 0 && !uiColors.ContainsKey(globalChatColor)) + { + globalChatColor = 0; + _configService.Current.ChatColor = 0; + _configService.Save(); + } + + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawColorCombo("Chat text color", Enumerable.Concat([0], uiColors.Keys), + i => i switch + { + 0 => (uiColors[ChatService.DefaultColor].Dark, "Plugin Default"), + _ => (uiColors[i].Dark, $"[{i}] Sample Text") + }, + i => { + _configService.Current.ChatColor = i; + _configService.Save(); + }, globalChatColor); + + int globalChatType = _configService.Current.ChatLogKind; + int globalChatTypeIdx = _syncshellChatTypes.FindIndex(x => globalChatType == (int)x.Item1); + + if (globalChatTypeIdx == -1) + globalChatTypeIdx = 0; + + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("Chat channel", Enumerable.Range(1, _syncshellChatTypes.Count - 1), i => $"{_syncshellChatTypes[i].Item2}", + i => { + if (_configService.Current.ChatLogKind == (int)_syncshellChatTypes[i].Item1) + return; + _configService.Current.ChatLogKind = (int)_syncshellChatTypes[i].Item1; + _chatService.PrintChannelExample($"Selected channel: {_syncshellChatTypes[i].Item2}"); + _configService.Save(); + }, globalChatTypeIdx); + _uiShared.DrawHelpText("FFXIV chat channel to output chat messages on."); + + ImGui.SetWindowFontScale(0.6f); + _uiShared.BigText("\"Chat 2\" Plugin Integration"); + ImGui.SetWindowFontScale(1.0f); + + var extraChatTags = _configService.Current.ExtraChatTags; + if (ImGui.Checkbox("Tag messages as ExtraChat", ref extraChatTags)) + { + _configService.Current.ExtraChatTags = extraChatTags; + if (!extraChatTags) + _configService.Current.ExtraChatAPI = false; + _configService.Save(); + } + _uiShared.DrawHelpText("If enabled, messages will be filtered under the category \"ExtraChat channels: All\".\n\nThis works even if ExtraChat is also installed and enabled."); + + ImGui.Separator(); + + _uiShared.BigText("Syncshell Settings"); + + if (!ApiController.ServerAlive) + { + ImGui.TextUnformatted("Connect to the server to configure individual syncshell settings."); + return; + } + + if (_pairManager.Groups.Count == 0) + { + ImGui.TextUnformatted("Once you join a syncshell you can configure its chat settings here."); + return; + } + + foreach (var group in _pairManager.Groups.OrderBy(k => k.Key.GID, StringComparer.Ordinal)) + { + var gid = group.Key.GID; + using var pushId = ImRaii.PushId(gid); + + var shellConfig = _serverConfigurationManager.GetShellConfigForGid(gid); + var shellNumber = shellConfig.ShellNumber; + var shellEnabled = shellConfig.Enabled; + var shellName = _serverConfigurationManager.GetNoteForGid(gid) ?? group.Key.AliasOrGID; + + if (shellEnabled) + shellName = $"[{shellNumber}] {shellName}"; + + ImGui.SetWindowFontScale(0.6f); + _uiShared.BigText(shellName); + ImGui.SetWindowFontScale(1.0f); + + using var pushIndent = ImRaii.PushIndent(); + + if (ImGui.Checkbox($"Enable chat for this syncshell##{gid}", ref shellEnabled)) + { + // If there is an active group with the same syncshell number, pick a new one + int nextNumber = 1; + bool conflict = false; + foreach (var otherGroup in _pairManager.Groups) + { + if (gid.Equals(otherGroup.Key.GID, StringComparison.Ordinal)) continue; + var otherShellConfig = _serverConfigurationManager.GetShellConfigForGid(otherGroup.Key.GID); + if (otherShellConfig.Enabled && otherShellConfig.ShellNumber == shellNumber) + conflict = true; + nextNumber = Math.Max(nextNumber, otherShellConfig.ShellNumber) + 1; + } + if (conflict) + shellConfig.ShellNumber = nextNumber; + shellConfig.Enabled = shellEnabled; + _serverConfigurationManager.SaveShellConfigForGid(gid, shellConfig); + } + + using var pushDisabled = ImRaii.Disabled(!shellEnabled); + + ImGui.SetNextItemWidth(50 * ImGuiHelpers.GlobalScale); + + // _uiShared.DrawCombo() remembers the selected option -- we don't want that, because the value can change + if (ImGui.BeginCombo("Syncshell number##{gid}", $"{shellNumber}")) + { + // Same hard-coded number in CommandManagerService + for (int i = 1; i <= ChatService.CommandMaxNumber; ++i) + { + if (ImGui.Selectable($"{i}", i == shellNumber)) + { + // Find an active group with the same syncshell number as selected, and swap it + // This logic can leave duplicate IDs present in the config but its not critical + foreach (var otherGroup in _pairManager.Groups) + { + if (gid.Equals(otherGroup.Key.GID, StringComparison.Ordinal)) continue; + var otherShellConfig = _serverConfigurationManager.GetShellConfigForGid(otherGroup.Key.GID); + if (otherShellConfig.Enabled && otherShellConfig.ShellNumber == i) + { + otherShellConfig.ShellNumber = shellNumber; + _serverConfigurationManager.SaveShellConfigForGid(otherGroup.Key.GID, otherShellConfig); + break; + } + } + shellConfig.ShellNumber = i; + _serverConfigurationManager.SaveShellConfigForGid(gid, shellConfig); + } + } + ImGui.EndCombo(); + } + + if (shellConfig.Color != 0 && !uiColors.ContainsKey(shellConfig.Color)) + { + shellConfig.Color = 0; + _serverConfigurationManager.SaveShellConfigForGid(gid, shellConfig); + } + + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawColorCombo($"Chat text color##{gid}", Enumerable.Concat([0], uiColors.Keys), + i => i switch + { + 0 => (uiColors[globalChatColor > 0 ? globalChatColor : ChatService.DefaultColor].Dark, "(use global setting)"), + _ => (uiColors[i].Dark, $"[{i}] Sample Text") + }, + i => { + shellConfig.Color = i; + _serverConfigurationManager.SaveShellConfigForGid(gid, shellConfig); + }, shellConfig.Color); + + int shellChatTypeIdx = _syncshellChatTypes.FindIndex(x => shellConfig.LogKind == (int)x.Item1); + + if (shellChatTypeIdx == -1) + shellChatTypeIdx = 0; + + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo($"Chat channel##{gid}", Enumerable.Range(0, _syncshellChatTypes.Count), i => $"{_syncshellChatTypes[i].Item2}", + i => { + shellConfig.LogKind = (int)_syncshellChatTypes[i].Item1; + _serverConfigurationManager.SaveShellConfigForGid(gid, shellConfig); + }, shellChatTypeIdx); + _uiShared.DrawHelpText("Override the FFXIV chat channel used for this syncshell."); + } + } + + private void DrawAdvanced() + { + _lastTab = "Advanced"; + + _uiShared.BigText("Advanced"); + + bool mareApi = _configService.Current.MareAPI; + if (ImGui.Checkbox("Enable Snowcloak Sync API", ref mareApi)) + { + _configService.Current.MareAPI = mareApi; + _configService.Save(); + _ipcProvider.HandleMareImpersonation(); + } + _uiShared.DrawHelpText("Enables handling of the Snowcloak Sync API. This currently includes:\n\n" + + " - MCDF loading support for other plugins\n" + + " - Blocking Moodles applications to paired users\n\n" + + "If the Snowcloak Sync plugin is loaded while this option is enabled, control of its API will be relinquished."); + + using (_ = ImRaii.PushIndent()) + { + ImGui.SameLine(300.0f * ImGuiHelpers.GlobalScale); + if (_ipcProvider.ImpersonationActive) + { + UiSharedService.ColorTextWrapped("Snowcloak API active!", ImGuiColors.HealerGreen); + } + else + { + if (!mareApi) + UiSharedService.ColorTextWrapped("Snowcloak API inactive: Option is disabled", ImGuiColors.DalamudYellow); + else if (_ipcProvider.MarePluginEnabled) + UiSharedService.ColorTextWrapped("Snowcloak API inactive: Snowcloak plugin is loaded", ImGuiColors.DalamudYellow); + else + UiSharedService.ColorTextWrapped("Snowcloak API inactive: Unknown reason", ImGuiColors.DalamudRed); + } + } + + bool logEvents = _configService.Current.LogEvents; + if (ImGui.Checkbox("Log Event Viewer data to disk", ref logEvents)) + { + _configService.Current.LogEvents = logEvents; + _configService.Save(); + } + + ImGui.SameLine(300.0f * ImGuiHelpers.GlobalScale); + if (_uiShared.IconTextButton(FontAwesomeIcon.NotesMedical, "Open Event Viewer")) + { + Mediator.Publish(new UiToggleMessage(typeof(EventViewerUI))); + } + + bool holdCombatApplication = _configService.Current.HoldCombatApplication; + if (ImGui.Checkbox("Hold application during combat", ref holdCombatApplication)) + { + if (!holdCombatApplication) + Mediator.Publish(new CombatOrPerformanceEndMessage()); + _configService.Current.HoldCombatApplication = holdCombatApplication; + _configService.Save(); + } + + bool serializedApplications = _configService.Current.SerialApplication; + if (ImGui.Checkbox("Serialized player applications", ref serializedApplications)) + { + _configService.Current.SerialApplication = serializedApplications; + _configService.Save(); + } + _uiShared.DrawHelpText("Experimental - May reduce issues in crowded areas"); + + ImGui.Separator(); + _uiShared.BigText("Debug"); +#if DEBUG + if (LastCreatedCharacterData != null && ImGui.TreeNode("Last created character data")) + { + foreach (var l in JsonSerializer.Serialize(LastCreatedCharacterData, new JsonSerializerOptions() { WriteIndented = true }).Split('\n')) + { + ImGui.TextUnformatted($"{l}"); + } + + ImGui.TreePop(); + } +#endif + if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "[DEBUG] Copy Last created Character Data to clipboard")) + { + if (LastCreatedCharacterData != null) + { + ImGui.SetClipboardText(JsonSerializer.Serialize(LastCreatedCharacterData, new JsonSerializerOptions() { WriteIndented = true })); + } + else + { + ImGui.SetClipboardText("ERROR: No created character data, cannot copy."); + } + } + UiSharedService.AttachToolTip("Use this when reporting mods being rejected from the server."); + + _uiShared.DrawCombo("Log Level", Enum.GetValues(), (l) => l.ToString(), (l) => + { + _configService.Current.LogLevel = l; + _configService.Save(); + }, _configService.Current.LogLevel); + + bool logPerformance = _configService.Current.LogPerformance; + if (ImGui.Checkbox("Log Performance Counters", ref logPerformance)) + { + _configService.Current.LogPerformance = logPerformance; + _configService.Save(); + } + _uiShared.DrawHelpText("Enabling this can incur a (slight) performance impact. Enabling this for extended periods of time is not recommended."); + + using (ImRaii.Disabled(!logPerformance)) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats to /xllog")) + { + _performanceCollector.PrintPerformanceStats(); + } + ImGui.SameLine(); + if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Print Performance Stats (last 60s) to /xllog")) + { + _performanceCollector.PrintPerformanceStats(60); + } + } + + if (ImGui.TreeNode("Active Character Blocks")) + { + var onlinePairs = _pairManager.GetOnlineUserPairs(); + foreach (var pair in onlinePairs) + { + if (pair.IsApplicationBlocked) + { + ImGui.TextUnformatted(pair.PlayerName); + ImGui.SameLine(); + ImGui.TextUnformatted(string.Join(", ", pair.HoldApplicationReasons)); + } + } + } + } + + private void DrawFileStorageSettings() + { + _lastTab = "FileCache"; + + _uiShared.BigText("Export MCDF"); + + ImGuiHelpers.ScaledDummy(10); + + UiSharedService.ColorTextWrapped("Exporting MCDF has moved.", ImGuiColors.DalamudYellow); + ImGuiHelpers.ScaledDummy(5); + UiSharedService.TextWrapped("It is now found in the Main UI under \"Character Data Hub\""); + if (_uiShared.IconTextButton(FontAwesomeIcon.Running, "Open Character Data Hub")) + { + Mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi))); + } + + ImGui.Separator(); + + _uiShared.BigText("Storage"); + + UiSharedService.TextWrapped("Snowcloak stores downloaded files from paired people permanently. This is to improve loading performance and requiring less downloads. " + + "The storage governs itself by clearing data beyond the set storage size. Please set the storage size accordingly. It is not necessary to manually clear the storage."); + + _uiShared.DrawFileScanState(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Monitoring Penumbra Folder: " + (_cacheMonitor.PenumbraWatcher?.Path ?? "Not monitoring")); + if (string.IsNullOrEmpty(_cacheMonitor.PenumbraWatcher?.Path)) + { + ImGui.SameLine(); + using var id = ImRaii.PushId("penumbraMonitor"); + if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowsToCircle, "Try to reinitialize Monitor")) + { + _cacheMonitor.StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory); + } + } + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Monitoring Snowcloak Storage Folder: " + (_cacheMonitor.MareWatcher?.Path ?? "Not monitoring")); + if (string.IsNullOrEmpty(_cacheMonitor.MareWatcher?.Path)) + { + ImGui.SameLine(); + using var id = ImRaii.PushId("mareMonitor"); + if (_uiShared.IconTextButton(FontAwesomeIcon.ArrowsToCircle, "Try to reinitialize Monitor")) + { + _cacheMonitor.StartMareWatcher(_configService.Current.CacheFolder); + } + } + if (_cacheMonitor.MareWatcher == null || _cacheMonitor.PenumbraWatcher == null) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.Play, "Resume Monitoring")) + { + _cacheMonitor.StartMareWatcher(_configService.Current.CacheFolder); + _cacheMonitor.StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory); + _cacheMonitor.InvokeScan(); + } + UiSharedService.AttachToolTip("Attempts to resume monitoring for both Penumbra and Snowcloak Storage. " + + "Resuming the monitoring will also force a full scan to run." + Environment.NewLine + + "If the button remains present after clicking it, consult /xllog for errors"); + } + else + { + using (ImRaii.Disabled(!UiSharedService.CtrlPressed())) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.Stop, "Stop Monitoring")) + { + _cacheMonitor.StopMonitoring(); + } + } + UiSharedService.AttachToolTip("Stops the monitoring for both Penumbra and Snowcloak Storage. " + + "Do not stop the monitoring, unless you plan to move the Penumbra and Snowcloak Storage folders, to ensure correct functionality of Snowcloak." + Environment.NewLine + + "If you stop the monitoring to move folders around, resume it after you are finished moving the files." + + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); + } + + _uiShared.DrawCacheDirectorySetting(); + ImGui.AlignTextToFramePadding(); + if (_cacheMonitor.FileCacheSize >= 0) + ImGui.TextUnformatted($"Currently utilized local storage: {_cacheMonitor.FileCacheSize / 1024.0 / 1024.0 / 1024.0:0.00} GiB"); + else + ImGui.TextUnformatted($"Currently utilized local storage: Calculating..."); + bool isLinux = _dalamudUtilService.IsWine; + if (!isLinux) + ImGui.TextUnformatted($"Remaining space free on drive: {_cacheMonitor.FileCacheDriveFree / 1024.0 / 1024.0 / 1024.0:0.00} GiB"); + bool useFileCompactor = _configService.Current.UseCompactor; + if (!useFileCompactor && !isLinux) + { + UiSharedService.ColorTextWrapped("Hint: To free up space when using Snowcloak consider enabling the File Compactor", ImGuiColors.DalamudYellow); + } + if (isLinux || !_cacheMonitor.StorageisNTFS) ImGui.BeginDisabled(); + if (ImGui.Checkbox("Use file compactor", ref useFileCompactor)) + { + _configService.Current.UseCompactor = useFileCompactor; + _configService.Save(); + } + _uiShared.DrawHelpText("The file compactor can massively reduce your saved files. It might incur a minor penalty on loading files on a slow CPU." + Environment.NewLine + + "It is recommended to leave it enabled to save on space."); + + if (!_fileCompactor.MassCompactRunning) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.FileArchive, "Compact all files in storage")) + { + _ = Task.Run(() => + { + _fileCompactor.CompactStorage(compress: true); + _cacheMonitor.RecalculateFileCacheSize(CancellationToken.None); + }); + } + UiSharedService.AttachToolTip("This will run compression on all files in your current storage folder." + Environment.NewLine + + "You do not need to run this manually if you keep the file compactor enabled."); + ImGui.SameLine(); + if (_uiShared.IconTextButton(FontAwesomeIcon.File, "Decompact all files in storage")) + { + _ = Task.Run(() => + { + _fileCompactor.CompactStorage(compress: false); + _cacheMonitor.RecalculateFileCacheSize(CancellationToken.None); + }); + } + UiSharedService.AttachToolTip("This will run decompression on all files in your current storage folder."); + } + else + { + UiSharedService.ColorText($"File compactor currently running ({_fileCompactor.Progress})", ImGuiColors.DalamudYellow); + } + if (isLinux || !_cacheMonitor.StorageisNTFS) + { + ImGui.EndDisabled(); + ImGui.TextUnformatted("The file compactor is only available on Windows and NTFS drives."); + } + ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); + + ImGui.Separator(); + UiSharedService.TextWrapped("File Storage validation can make sure that all files in your local storage folder are valid. " + + "Run the validation before you clear the Storage for no reason. " + Environment.NewLine + + "This operation, depending on how many files you have in your storage, can take a while and will be CPU and drive intensive."); + using (ImRaii.Disabled(_validationTask != null && !_validationTask.IsCompleted)) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.Check, "Start File Storage Validation")) + { + _validationCts?.Cancel(); + _validationCts?.Dispose(); + _validationCts = new(); + var token = _validationCts.Token; + _validationTask = Task.Run(() => _fileCacheManager.ValidateLocalIntegrity(_validationProgress, token)); + } + } + if (_validationTask != null && !_validationTask.IsCompleted) + { + ImGui.SameLine(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Times, "Cancel")) + { + _validationCts?.Cancel(); + } + } + + if (_validationTask != null) + { + using (ImRaii.PushIndent(20f)) + { + if (_validationTask.IsCompleted) + { + UiSharedService.TextWrapped($"The storage validation has completed and removed {_validationTask.Result.Count} invalid files from storage."); + } + else + { + + UiSharedService.TextWrapped($"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}"); + UiSharedService.TextWrapped($"Current item: {_currentProgress.Item3.ResolvedFilepath}"); + } + } + } + ImGui.Separator(); + + ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); + ImGui.TextUnformatted("To clear the local storage accept the following disclaimer"); + ImGui.Indent(); + ImGui.Checkbox("##readClearCache", ref _readClearCache); + ImGui.SameLine(); + UiSharedService.TextWrapped("I understand that: " + Environment.NewLine + "- By clearing the local storage I put the file servers of my connected service under extra strain by having to redownload all data." + + Environment.NewLine + "- This is not a step to try to fix sync issues." + + Environment.NewLine + "- This can make the situation of not getting other players data worse in situations of heavy file server load."); + if (!_readClearCache) + ImGui.BeginDisabled(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Clear local storage") && UiSharedService.CtrlPressed() && _readClearCache) + { + _ = Task.Run(() => + { + foreach (var file in Directory.GetFiles(_configService.Current.CacheFolder)) + { + File.Delete(file); + } + }); + } + UiSharedService.AttachToolTip("You normally do not need to do this. THIS IS NOT SOMETHING YOU SHOULD BE DOING TO TRY TO FIX SYNC ISSUES." + Environment.NewLine + + "This will solely remove all downloaded data from all players and will require you to re-download everything again." + Environment.NewLine + + "Snowcloak's storage is self-clearing and will not surpass the limit you have set it to." + Environment.NewLine + + "If you still think you need to do this hold CTRL while pressing the button."); + if (!_readClearCache) + ImGui.EndDisabled(); + ImGui.Unindent(); + } + + private void DrawGeneral() + { + if (!string.Equals(_lastTab, "General", StringComparison.OrdinalIgnoreCase)) + { + _notesSuccessfullyApplied = null; + } + + _lastTab = "General"; + + _uiShared.BigText("Notes"); + if (_uiShared.IconTextButton(FontAwesomeIcon.StickyNote, "Export all your user notes to clipboard")) + { + ImGui.SetClipboardText(UiSharedService.GetNotes(_pairManager.DirectPairs.UnionBy(_pairManager.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); + } + + 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", ImGuiColors.HealerGreen); + } + else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value) + { + UiSharedService.ColorTextWrapped("Attempt to import notes from clipboard failed. Check formatting and try again", ImGuiColors.DalamudRed); + } + + var openPopupOnAddition = _configService.Current.OpenPopupOnAdd; + + 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."); + + ImGui.Separator(); + _uiShared.BigText("UI"); + var showCharacterNames = _configService.Current.ShowCharacterNames; + var showVisibleSeparate = _configService.Current.ShowVisibleUsersSeparately; + var showOfflineSeparate = _configService.Current.ShowOfflineUsersSeparately; + var showProfiles = _configService.Current.ProfilesShow; + var showNsfwProfiles = _configService.Current.ProfilesAllowNsfw; + var profileDelay = _configService.Current.ProfileDelay; + var profileOnRight = _configService.Current.ProfilePopoutRight; + var enableRightClickMenu = _configService.Current.EnableRightClickMenus; + var enableDtrEntry = _configService.Current.EnableDtrEntry; + var showUidInDtrTooltip = _configService.Current.ShowUidInDtrTooltip; + var preferNoteInDtrTooltip = _configService.Current.PreferNoteInDtrTooltip; + var useColorsInDtr = _configService.Current.UseColorsInDtr; + var dtrColorsDefault = _configService.Current.DtrColorsDefault; + var dtrColorsNotConnected = _configService.Current.DtrColorsNotConnected; + var dtrColorsPairsInRange = _configService.Current.DtrColorsPairsInRange; + + if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu)) + { + _configService.Current.EnableRightClickMenus = enableRightClickMenu; + _configService.Save(); + } + _uiShared.DrawHelpText("This will add Snowcloak related right click menu entries in the game UI on paired players."); + + 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 Snowcloak 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)) + { + _configService.Current.ShowUidInDtrTooltip = showUidInDtrTooltip; + _configService.Save(); + } + + if (ImGui.Checkbox("Prefer notes over player names in tooltip", ref preferNoteInDtrTooltip)) + { + _configService.Current.PreferNoteInDtrTooltip = preferNoteInDtrTooltip; + _configService.Save(); + } + + ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("Server Info Bar style", Enumerable.Range(0, DtrEntry.NumStyles), (i) => DtrEntry.RenderDtrStyle(i, "123"), + (i) => + { + _configService.Current.DtrStyle = i; + _configService.Save(); + }, _configService.Current.DtrStyle); + + if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr)) + { + _configService.Current.UseColorsInDtr = useColorsInDtr; + _configService.Save(); + } + + using (ImRaii.Disabled(!useColorsInDtr)) + { + using var indent2 = ImRaii.PushIndent(); + if (InputDtrColors("Default", ref dtrColorsDefault)) + { + _configService.Current.DtrColorsDefault = dtrColorsDefault; + _configService.Save(); + } + + ImGui.SameLine(); + if (InputDtrColors("Not Connected", ref dtrColorsNotConnected)) + { + _configService.Current.DtrColorsNotConnected = dtrColorsNotConnected; + _configService.Save(); + } + + ImGui.SameLine(); + if (InputDtrColors("Pairs in Range", ref dtrColorsPairsInRange)) + { + _configService.Current.DtrColorsPairsInRange = dtrColorsPairsInRange; + _configService.Save(); + } + } + } + + var useNameColors = _configService.Current.UseNameColors; + var nameColors = _configService.Current.NameColors; + var autoPausedNameColors = _configService.Current.BlockedNameColors; + if (ImGui.Checkbox("Color nameplates of paired players", ref useNameColors)) + { + _configService.Current.UseNameColors = useNameColors; + _configService.Save(); + _guiHookService.RequestRedraw(); + } + + using (ImRaii.Disabled(!useNameColors)) + { + using var indent = ImRaii.PushIndent(); + if (InputDtrColors("Character Name Color", ref nameColors)) + { + _configService.Current.NameColors = nameColors; + _configService.Save(); + _guiHookService.RequestRedraw(); + } + + ImGui.SameLine(); + + if (InputDtrColors("Blocked Character Color", ref autoPausedNameColors)) + { + _configService.Current.BlockedNameColors = autoPausedNameColors; + _configService.Save(); + _guiHookService.RequestRedraw(); + } + } + + if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) + { + _configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate; + _configService.Save(); + } + _uiShared.DrawHelpText("This will show all currently visible users in a special 'Visible' group in the main UI."); + + if (ImGui.Checkbox("Show separate Offline group", ref showOfflineSeparate)) + { + _configService.Current.ShowOfflineUsersSeparately = showOfflineSeparate; + _configService.Save(); + } + _uiShared.DrawHelpText("This will show all currently offline users in a special 'Offline' group in the main UI."); + + if (ImGui.Checkbox("Show player names", ref showCharacterNames)) + { + _configService.Current.ShowCharacterNames = showCharacterNames; + _configService.Save(); + } + _uiShared.DrawHelpText("This will show character names instead of UIDs when possible"); + + if (ImGui.Checkbox("Show Profiles on Hover", ref showProfiles)) + { + Mediator.Publish(new ClearProfileDataMessage()); + _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"); + ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); + 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 ClearProfileDataMessage()); + _configService.Current.ProfilesAllowNsfw = showNsfwProfiles; + _configService.Save(); + } + _uiShared.DrawHelpText("Will show profiles that have the NSFW tag enabled"); + + ImGui.Separator(); + + var disableOptionalPluginWarnings = _configService.Current.DisableOptionalPluginWarnings; + var onlineNotifs = _configService.Current.ShowOnlineNotifications; + var onlineNotifsPairsOnly = _configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs; + var onlineNotifsNamedOnly = _configService.Current.ShowOnlineNotificationsOnlyForNamedPairs; + _uiShared.BigText("Notifications"); + + ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("Info Notification Display##settingsUi", (NotificationLocation[])Enum.GetValues(typeof(NotificationLocation)), (i) => i.ToString(), + (i) => + { + _configService.Current.InfoNotification = i; + _configService.Save(); + }, _configService.Current.InfoNotification); + _uiShared.DrawHelpText("The location where \"Info\" notifications will display." + + Environment.NewLine + "'Nowhere' will not show any Info notifications" + + Environment.NewLine + "'Chat' will print Info notifications in chat" + + Environment.NewLine + "'Toast' will show Warning toast notifications in the bottom right corner" + + Environment.NewLine + "'Both' will show chat as well as the toast notification"); + + ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("Warning Notification Display##settingsUi", (NotificationLocation[])Enum.GetValues(typeof(NotificationLocation)), (i) => i.ToString(), + (i) => + { + _configService.Current.WarningNotification = i; + _configService.Save(); + }, _configService.Current.WarningNotification); + _uiShared.DrawHelpText("The location where \"Warning\" notifications will display." + + Environment.NewLine + "'Nowhere' will not show any Warning notifications" + + Environment.NewLine + "'Chat' will print Warning notifications in chat" + + Environment.NewLine + "'Toast' will show Warning toast notifications in the bottom right corner" + + Environment.NewLine + "'Both' will show chat as well as the toast notification"); + + ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); + _uiShared.DrawCombo("Error Notification Display##settingsUi", (NotificationLocation[])Enum.GetValues(typeof(NotificationLocation)), (i) => i.ToString(), + (i) => + { + _configService.Current.ErrorNotification = i; + _configService.Save(); + }, _configService.Current.ErrorNotification); + _uiShared.DrawHelpText("The location where \"Error\" notifications will display." + + Environment.NewLine + "'Nowhere' will not show any Error notifications" + + Environment.NewLine + "'Chat' will print Error notifications in chat" + + Environment.NewLine + "'Toast' will show Error toast notifications in the bottom right corner" + + Environment.NewLine + "'Both' will show chat as well as the toast notification"); + + if (ImGui.Checkbox("Disable optional plugin warnings", ref disableOptionalPluginWarnings)) + { + _configService.Current.DisableOptionalPluginWarnings = disableOptionalPluginWarnings; + _configService.Save(); + } + _uiShared.DrawHelpText("Enabling this will not show any \"Warning\" labeled messages for missing optional plugins."); + if (ImGui.Checkbox("Enable online notifications", ref onlineNotifs)) + { + _configService.Current.ShowOnlineNotifications = onlineNotifs; + _configService.Save(); + } + _uiShared.DrawHelpText("Enabling this will show a small notification (type: Info) in the bottom right corner when pairs go online."); + + using (ImRaii.Disabled(!onlineNotifs)) + { + using var indent = ImRaii.PushIndent(); + if (ImGui.Checkbox("Notify only for individual pairs", ref onlineNotifsPairsOnly)) + { + _configService.Current.ShowOnlineNotificationsOnlyForIndividualPairs = onlineNotifsPairsOnly; + _configService.Save(); + } + _uiShared.DrawHelpText("Enabling this will only show online notifications (type: Info) for individual pairs."); + if (ImGui.Checkbox("Notify only for named pairs", ref onlineNotifsNamedOnly)) + { + _configService.Current.ShowOnlineNotificationsOnlyForNamedPairs = onlineNotifsNamedOnly; + _configService.Save(); + } + _uiShared.DrawHelpText("Enabling this will only show online notifications (type: Info) for pairs where you have set an individual note."); + } + } + + private bool _perfUnapplied = false; + + private void DrawPerformance() + { + _uiShared.BigText("Performance Settings"); + UiSharedService.TextWrapped("The configuration options here are to give you more informed warnings and automation when it comes to other performance-intensive synced players."); + ImGui.Separator(); + bool recalculatePerformance = false; + string? recalculatePerformanceUID = null; + + _uiShared.BigText("Global Configuration"); + + bool alwaysShrinkTextures = _playerPerformanceConfigService.Current.TextureShrinkMode == TextureShrinkMode.Always; + bool deleteOriginalTextures = _playerPerformanceConfigService.Current.TextureShrinkDeleteOriginal; + + using (ImRaii.Disabled(deleteOriginalTextures)) + { + if (ImGui.Checkbox("Shrink downloaded textures", ref alwaysShrinkTextures)) + { + if (alwaysShrinkTextures) + _playerPerformanceConfigService.Current.TextureShrinkMode = TextureShrinkMode.Always; + else + _playerPerformanceConfigService.Current.TextureShrinkMode = TextureShrinkMode.Never; + _playerPerformanceConfigService.Save(); + recalculatePerformance = true; + _cacheMonitor.ClearSubstStorage(); + } + } + _uiShared.DrawHelpText("Automatically shrinks texture resolution of synced players to reduce VRAM utilization." + UiSharedService.TooltipSeparator + + "Texture Size Limit (DXT/BC5/BC7 Compressed): 2048x2048" + Environment.NewLine + + "Texture Size Limit (A8R8G8B8 Uncompressed): 1024x1024" + UiSharedService.TooltipSeparator + + "Enable to reduce lag in large crowds." + Environment.NewLine + + "Disable this for higher quality during GPose."); + + using (ImRaii.Disabled(!alwaysShrinkTextures || _cacheMonitor.FileCacheSize < 0)) + { + using var indent = ImRaii.PushIndent(); + if (ImGui.Checkbox("Delete original textures from disk", ref deleteOriginalTextures)) + { + _playerPerformanceConfigService.Current.TextureShrinkDeleteOriginal = deleteOriginalTextures; + _playerPerformanceConfigService.Save(); + _ = Task.Run(() => + { + _cacheMonitor.DeleteSubstOriginals(); + _cacheMonitor.RecalculateFileCacheSize(CancellationToken.None); + }); + } + _uiShared.DrawHelpText("Deletes original, full-sized, textures from disk after downloading and shrinking." + UiSharedService.TooltipSeparator + + "Caution!!! This will cause a re-download of all textures when the shrink option is disabled."); + } + + var totalVramBytes = _pairManager.GetOnlineUserPairs().Where(p => p.IsVisible && p.LastAppliedApproximateVRAMBytes > 0).Sum(p => p.LastAppliedApproximateVRAMBytes); + + ImGui.TextUnformatted("Current VRAM utilization by all nearby players:"); + ImGui.SameLine(); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen, totalVramBytes < 2.0 * 1024.0 * 1024.0 * 1024.0)) + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, totalVramBytes >= 4.0 * 1024.0 * 1024.0 * 1024.0)) + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, totalVramBytes >= 6.0 * 1024.0 * 1024.0 * 1024.0)) + ImGui.TextUnformatted($"{totalVramBytes / 1024.0 / 1024.0 / 1024.0:0.00} GiB"); + + ImGui.Separator(); + _uiShared.BigText("Individual Limits"); + bool autoPause = _playerPerformanceConfigService.Current.AutoPausePlayersExceedingThresholds; + if (ImGui.Checkbox("Automatically block players exceeding thresholds", ref autoPause)) + { + _playerPerformanceConfigService.Current.AutoPausePlayersExceedingThresholds = autoPause; + _playerPerformanceConfigService.Save(); + recalculatePerformance = true; + } + _uiShared.DrawHelpText("When enabled, it will automatically block the modded appearance of all players that exceed the thresholds defined below." + Environment.NewLine + + "Will print a warning in chat when a player is blocked automatically."); + using (ImRaii.Disabled(!autoPause)) + { + using var indent = ImRaii.PushIndent(); + var notifyDirectPairs = _playerPerformanceConfigService.Current.NotifyAutoPauseDirectPairs; + var notifyGroupPairs = _playerPerformanceConfigService.Current.NotifyAutoPauseGroupPairs; + if (ImGui.Checkbox("Display auto-block warnings for individual pairs", ref notifyDirectPairs)) + { + _playerPerformanceConfigService.Current.NotifyAutoPauseDirectPairs = notifyDirectPairs; + _playerPerformanceConfigService.Save(); + } + if (ImGui.Checkbox("Display auto-block warnings for syncshell pairs", ref notifyGroupPairs)) + { + _playerPerformanceConfigService.Current.NotifyAutoPauseGroupPairs = notifyGroupPairs; + _playerPerformanceConfigService.Save(); + } + var vramAuto = _playerPerformanceConfigService.Current.VRAMSizeAutoPauseThresholdMiB; + var trisAuto = _playerPerformanceConfigService.Current.TrisAutoPauseThresholdThousands; + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("Auto Block VRAM threshold", ref vramAuto)) + { + _playerPerformanceConfigService.Current.VRAMSizeAutoPauseThresholdMiB = vramAuto; + _playerPerformanceConfigService.Save(); + _perfUnapplied = true; + } + ImGui.SameLine(); + ImGui.Text("(MiB)"); + _uiShared.DrawHelpText("When a loading in player and their VRAM usage exceeds this amount, automatically blocks the synced player." + UiSharedService.TooltipSeparator + + "Default: 550 MiB"); + ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale); + if (ImGui.InputInt("Auto Block Triangle threshold", ref trisAuto)) + { + _playerPerformanceConfigService.Current.TrisAutoPauseThresholdThousands = trisAuto; + _playerPerformanceConfigService.Save(); + _perfUnapplied = true; + } + ImGui.SameLine(); + ImGui.Text("(thousand triangles)"); + _uiShared.DrawHelpText("When a loading in player and their triangle count exceeds this amount, automatically blocks the synced player." + UiSharedService.TooltipSeparator + + "Default: 375 thousand"); + using (ImRaii.Disabled(!_perfUnapplied)) + { + if (ImGui.Button("Apply Changes Now")) + { + recalculatePerformance = true; + _perfUnapplied = false; + } + } + } + +#region Whitelist + ImGui.Separator(); + _uiShared.BigText("Whitelisted UIDs"); + bool ignoreDirectPairs = _playerPerformanceConfigService.Current.IgnoreDirectPairs; + if (ImGui.Checkbox("Whitelist all individual pairs", ref ignoreDirectPairs)) + { + _playerPerformanceConfigService.Current.IgnoreDirectPairs = ignoreDirectPairs; + _playerPerformanceConfigService.Save(); + recalculatePerformance = true; + } + _uiShared.DrawHelpText("Individual pairs will never be affected by auto blocks."); + ImGui.Dummy(new Vector2(5)); + UiSharedService.TextWrapped("The entries in the list below will be not have auto block thresholds enforced."); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + var whitelistPos = ImGui.GetCursorPos(); + ImGui.SetCursorPosX(240 * ImGuiHelpers.GlobalScale); + ImGui.InputText("##whitelistuid", ref _uidToAddForIgnore, 20); + using (ImRaii.Disabled(string.IsNullOrEmpty(_uidToAddForIgnore))) + { + ImGui.SetCursorPosX(240 * ImGuiHelpers.GlobalScale); + if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add UID/Vanity ID to whitelist")) + { + if (!_serverConfigurationManager.IsUidWhitelisted(_uidToAddForIgnore)) + { + _serverConfigurationManager.AddWhitelistUid(_uidToAddForIgnore); + recalculatePerformance = true; + recalculatePerformanceUID = _uidToAddForIgnore; + } + _uidToAddForIgnore = string.Empty; + } + } + ImGui.SetCursorPosX(240 * ImGuiHelpers.GlobalScale); + _uiShared.DrawHelpText("Hint: UIDs are case sensitive.\nVanity IDs are also acceptable."); + ImGui.Dummy(new Vector2(10)); + var playerList = _serverConfigurationManager.Whitelist; + if (_selectedEntry > playerList.Count - 1) + _selectedEntry = -1; + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + ImGui.SetCursorPosY(whitelistPos.Y); + using (var lb = ImRaii.ListBox("##whitelist")) + { + if (lb) + { + for (int i = 0; i < playerList.Count; i++) + { + bool shouldBeSelected = _selectedEntry == i; + if (ImGui.Selectable(playerList[i] + "##" + i, shouldBeSelected)) + { + _selectedEntry = i; + } + string? lastSeenName = _serverConfigurationManager.GetNameForUid(playerList[i]); + if (lastSeenName != null) + { + ImGui.SameLine(); + _uiShared.IconText(FontAwesomeIcon.InfoCircle); + UiSharedService.AttachToolTip($"Last seen name: {lastSeenName}"); + } + } + } + } + using (ImRaii.Disabled(_selectedEntry == -1)) + { + using var pushId = ImRaii.PushId("deleteSelectedWhitelist"); + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete selected UID")) + { + _serverConfigurationManager.RemoveWhitelistUid(_serverConfigurationManager.Whitelist[_selectedEntry]); + if (_selectedEntry > playerList.Count - 1) + --_selectedEntry; + _playerPerformanceConfigService.Save(); + recalculatePerformance = true; + } + } +#endregion Whitelist + +#region Blacklist + ImGui.Separator(); + _uiShared.BigText("Blacklisted UIDs"); + UiSharedService.TextWrapped("The entries in the list below will never have their characters displayed."); + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + var blacklistPos = ImGui.GetCursorPos(); + ImGui.SetCursorPosX(240 * ImGuiHelpers.GlobalScale); + ImGui.InputText("##uid", ref _uidToAddForIgnoreBlacklist, 20); + using (ImRaii.Disabled(string.IsNullOrEmpty(_uidToAddForIgnoreBlacklist))) + { + ImGui.SetCursorPosX(240 * ImGuiHelpers.GlobalScale); + if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Add UID/Vanity ID to blacklist")) + { + if (!_serverConfigurationManager.IsUidBlacklisted(_uidToAddForIgnoreBlacklist)) + { + _serverConfigurationManager.AddBlacklistUid(_uidToAddForIgnoreBlacklist); + recalculatePerformance = true; + recalculatePerformanceUID = _uidToAddForIgnoreBlacklist; + } + _uidToAddForIgnoreBlacklist = string.Empty; + } + } + _uiShared.DrawHelpText("Hint: UIDs are case sensitive.\nVanity IDs are also acceptable."); + ImGui.Dummy(new Vector2(10)); + var blacklist = _serverConfigurationManager.Blacklist; + if (_selectedEntryBlacklist > blacklist.Count - 1) + _selectedEntryBlacklist = -1; + ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); + ImGui.SetCursorPosY(blacklistPos.Y); + using (var lb = ImRaii.ListBox("##blacklist")) + { + if (lb) + { + for (int i = 0; i < blacklist.Count; i++) + { + bool shouldBeSelected = _selectedEntryBlacklist == i; + if (ImGui.Selectable(blacklist[i] + "##BL" + i, shouldBeSelected)) + { + _selectedEntryBlacklist = i; + } + string? lastSeenName = _serverConfigurationManager.GetNameForUid(blacklist[i]); + if (lastSeenName != null) + { + ImGui.SameLine(); + _uiShared.IconText(FontAwesomeIcon.InfoCircle); + UiSharedService.AttachToolTip($"Last seen name: {lastSeenName}"); + } + } + } + } + using (ImRaii.Disabled(_selectedEntryBlacklist == -1)) + { + using var pushId = ImRaii.PushId("deleteSelectedBlacklist"); + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete selected UID")) + { + _serverConfigurationManager.RemoveBlacklistUid(_serverConfigurationManager.Blacklist[_selectedEntryBlacklist]); + if (_selectedEntryBlacklist > blacklist.Count - 1) + --_selectedEntryBlacklist; + _playerPerformanceConfigService.Save(); + recalculatePerformance = true; + } + } +#endregion Blacklist + + if (recalculatePerformance) + Mediator.Publish(new RecalculatePerformanceMessage(recalculatePerformanceUID)); + } + + private static bool InputDtrColors(string label, ref DtrEntry.Colors colors) + { + using var id = ImRaii.PushId(label); + var innerSpacing = ImGui.GetStyle().ItemInnerSpacing.X; + var foregroundColor = ConvertColor(colors.Foreground); + var glowColor = ConvertColor(colors.Glow); + + var ret = ImGui.ColorEdit3("###foreground", ref foregroundColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Foreground Color - Set to pure black (#000000) to use the default color"); + + ImGui.SameLine(0.0f, innerSpacing); + ret |= ImGui.ColorEdit3("###glow", ref glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Glow Color - Set to pure black (#000000) to use the default color"); + + ImGui.SameLine(0.0f, innerSpacing); + ImGui.TextUnformatted(label); + + if (ret) + colors = new(ConvertBackColor(foregroundColor), ConvertBackColor(glowColor)); + + return ret; + + static Vector3 ConvertColor(uint color) + => unchecked(new((byte)color / 255.0f, (byte)(color >> 8) / 255.0f, (byte)(color >> 16) / 255.0f)); + + static uint ConvertBackColor(Vector3 color) + => byte.CreateSaturating(color.X * 255.0f) | ((uint)byte.CreateSaturating(color.Y * 255.0f) << 8) | ((uint)byte.CreateSaturating(color.Z * 255.0f) << 16); + } + + private void DrawServerConfiguration() + { + _lastTab = "Service Settings"; + if (ApiController.ServerAlive) + { + _uiShared.BigText("Service Actions"); + ImGuiHelpers.ScaledDummy(new Vector2(5, 5)); + if (ImGui.Button("Delete account")) + { + _deleteAccountPopupModalShown = true; + ImGui.OpenPopup("Delete your account?"); + } + + _uiShared.DrawHelpText("Completely deletes your currently connected account."); + + if (ImGui.BeginPopupModal("Delete your account?", ref _deleteAccountPopupModalShown, UiSharedService.PopupWindowFlags)) + { + UiSharedService.TextWrapped( + "Your account and all associated files and data on the service will be deleted."); + UiSharedService.TextWrapped("Your UID will be removed from all pairing lists."); + ImGui.TextUnformatted("Are you sure you want to continue?"); + ImGui.Separator(); + ImGui.Spacing(); + + var buttonSize = (ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - + ImGui.GetStyle().ItemSpacing.X) / 2; + + if (ImGui.Button("Delete account", new Vector2(buttonSize, 0))) + { + _ = Task.Run(ApiController.UserDelete); + _deleteAccountPopupModalShown = false; + Mediator.Publish(new SwitchToIntroUiMessage()); + } + + ImGui.SameLine(); + + if (ImGui.Button("Cancel##cancelDelete", new Vector2(buttonSize, 0))) + { + _deleteAccountPopupModalShown = false; + } + + UiSharedService.SetScaledWindowSize(325); + ImGui.EndPopup(); + } + ImGui.Separator(); + } + + _uiShared.BigText("Service & Character Settings"); + + var idx = _uiShared.DrawServiceSelection(); + var playerName = _dalamudUtilService.GetPlayerName(); + var playerWorldId = _dalamudUtilService.GetHomeWorldId(); + var worldData = _uiShared.WorldData.OrderBy(u => u.Value, StringComparer.Ordinal).ToDictionary(k => k.Key, k => k.Value); + string playerWorldName = worldData.GetValueOrDefault((ushort)playerWorldId, $"{playerWorldId}"); + + ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); + + var selectedServer = _serverConfigurationManager.GetServerByIndex(idx); + if (selectedServer == _serverConfigurationManager.CurrentServer) + { + if (_apiController.IsConnected) + UiSharedService.ColorTextWrapped("For any changes to be applied to the current service you need to reconnect to the service.", ImGuiColors.DalamudYellow); + } + + if (ImGui.BeginTabBar("serverTabBar")) + { + if (ImGui.BeginTabItem("Character Assignments")) + { + if (selectedServer.SecretKeys.Count > 0) + { + float windowPadding = ImGui.GetStyle().WindowPadding.X; + float itemSpacing = ImGui.GetStyle().ItemSpacing.X; + float longestName = 0.0f; + if (selectedServer.Authentications.Count > 0) + longestName = selectedServer.Authentications.Max(p => ImGui.CalcTextSize($"{p.CharacterName} @ Pandaemonium ").X); + float iconWidth; + + using (_ = _uiShared.IconFont.Push()) + iconWidth = ImGui.CalcTextSize(FontAwesomeIcon.Trash.ToIconString()).X; + + UiSharedService.ColorTextWrapped("Characters listed here will connect with the specified secret key.", ImGuiColors.DalamudYellow); + int i = 0; + foreach (var item in selectedServer.Authentications.ToList()) + { + using var charaId = ImRaii.PushId("selectedChara" + i); + + bool thisIsYou = string.Equals(playerName, item.CharacterName, StringComparison.OrdinalIgnoreCase) + && playerWorldId == item.WorldId; + + if (!worldData.TryGetValue((ushort)item.WorldId, out string? worldPreview)) + worldPreview = worldData.First().Value; + + _uiShared.IconText(thisIsYou ? FontAwesomeIcon.Star : FontAwesomeIcon.None); + + if (thisIsYou) + UiSharedService.AttachToolTip("Current character"); + + ImGui.SameLine(windowPadding + iconWidth + itemSpacing); + float beforeName = ImGui.GetCursorPosX(); + ImGui.TextUnformatted($"{item.CharacterName} @ {worldPreview}"); + float afterName = ImGui.GetCursorPosX(); + + ImGui.SameLine(afterName + (afterName - beforeName) + longestName + itemSpacing); + + var secretKeyIdx = item.SecretKeyIdx; + var keys = selectedServer.SecretKeys; + if (!keys.TryGetValue(secretKeyIdx, out var secretKey)) + { + secretKey = new(); + } + var friendlyName = secretKey.FriendlyName; + + ImGui.SetNextItemWidth(afterName - iconWidth - itemSpacing * 2 - windowPadding); + + string selectedKeyName = string.Empty; + if (selectedServer.SecretKeys.TryGetValue(item.SecretKeyIdx, out var selectedKey)) + selectedKeyName = selectedKey.FriendlyName; + + // _uiShared.DrawCombo() remembers the selected option -- we don't want that, because the value can change + if (ImGui.BeginCombo($"##{item.CharacterName}{i}", selectedKeyName)) + { + foreach (var key in selectedServer.SecretKeys) + { + if (ImGui.Selectable($"{key.Value.FriendlyName}##{i}", key.Key == item.SecretKeyIdx) + && key.Key != item.SecretKeyIdx) + { + item.SecretKeyIdx = key.Key; + _serverConfigurationManager.Save(); + } + } + ImGui.EndCombo(); + } + + ImGui.SameLine(); + + if (_uiShared.IconButton(FontAwesomeIcon.Trash)) + _serverConfigurationManager.RemoveCharacterFromServer(idx, item); + UiSharedService.AttachToolTip("Delete character assignment"); + + i++; + } + + ImGui.Separator(); + using (_ = ImRaii.Disabled(selectedServer.Authentications.Exists(c => + string.Equals(c.CharacterName, _uiShared.PlayerName, StringComparison.Ordinal) + && c.WorldId == _uiShared.WorldId + ))) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.User, "Add current character")) + { + _serverConfigurationManager.AddCurrentCharacterToServer(idx); + } + ImGui.SameLine(); + } + } + else + { + UiSharedService.ColorTextWrapped("You need to add a Secret Key first before adding Characters.", ImGuiColors.DalamudYellow); + } + + ImGui.EndTabItem(); + } + + if (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; + var keyInUse = selectedServer.Authentications.Exists(p => p.SecretKeyIdx == item.Key); + if (keyInUse) ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); + if (ImGui.InputText("Secret Key", ref key, 64, keyInUse ? ImGuiInputTextFlags.ReadOnly : default)) + { + item.Value.Key = key; + _serverConfigurationManager.Save(); + } + if (keyInUse) ImGui.PopStyleColor(); + + bool thisIsYou = selectedServer.Authentications.Any(a => + a.SecretKeyIdx == item.Key + && string.Equals(a.CharacterName, _uiShared.PlayerName, StringComparison.OrdinalIgnoreCase) + && a.WorldId == playerWorldId + ); + + bool disableAssignment = thisIsYou || item.Value.Key.IsNullOrEmpty(); + + using (_ = ImRaii.Disabled(disableAssignment)) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.User, "Assign current character")) + { + var currentAssignment = selectedServer.Authentications.Find(a => + string.Equals(a.CharacterName, _uiShared.PlayerName, StringComparison.OrdinalIgnoreCase) + && a.WorldId == playerWorldId + ); + + if (currentAssignment == null) + { + selectedServer.Authentications.Add(new Authentication() + { + CharacterName = playerName, + WorldId = playerWorldId, + SecretKeyIdx = item.Key + }); + } + else + { + currentAssignment.SecretKeyIdx = item.Key; + } + } + if (!disableAssignment) + UiSharedService.AttachToolTip($"Use this secret key for {playerName} @ {playerWorldName}"); + } + + ImGui.SameLine(); + using var disableDelete = ImRaii.Disabled(keyInUse); + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Secret Key") && UiSharedService.CtrlPressed()) + { + selectedServer.SecretKeys.Remove(item.Key); + _serverConfigurationManager.Save(); + } + if (!keyInUse) + UiSharedService.AttachToolTip("Hold CTRL to delete this secret key entry"); + + if (keyInUse) + { + UiSharedService.ColorTextWrapped("This key is currently assigned to a character and cannot be edited or deleted.", ImGuiColors.DalamudYellow); + } + + 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(); + } + + if (true) // Enable registration button for all servers + { + ImGui.SameLine(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Register a new Snowcloak account")) + { + _registrationInProgress = true; + _ = Task.Run(async () => { + try + { + var reply = await _registerService.RegisterAccount(CancellationToken.None).ConfigureAwait(false); + if (!reply.Success) + { + _logger.LogWarning("Registration failed: {err}", reply.ErrorMessage); + _registrationMessage = reply.ErrorMessage; + if (_registrationMessage.IsNullOrEmpty()) + _registrationMessage = "An unknown error occured. Please try again later."; + return; + } + _registrationMessage = "New account registered.\nPlease keep a copy of your secret key in case you need to reset your plugins, or to use it on another PC."; + _registrationSuccess = true; + selectedServer.SecretKeys.Add(selectedServer.SecretKeys.Any() ? selectedServer.SecretKeys.Max(p => p.Key) + 1 : 0, new SecretKey() + { + FriendlyName = reply.UID + $" (registered {DateTime.Now:yyyy-MM-dd})", + Key = reply.SecretKey ?? "" + }); + _serverConfigurationManager.Save(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Registration failed"); + _registrationSuccess = false; + _registrationMessage = "An unknown error occured. Please try again later."; + } + finally + { + _registrationInProgress = false; + } + }, CancellationToken.None); + } + if (_registrationInProgress) + { + ImGui.TextUnformatted("Sending request..."); + } + else if (!_registrationMessage.IsNullOrEmpty()) + { + if (!_registrationSuccess) + ImGui.TextColored(ImGuiColors.DalamudYellow, _registrationMessage); + else + ImGui.TextWrapped(_registrationMessage); + } + } + + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Service Settings")) + { + var serverName = selectedServer.ServerName; + var serverUri = selectedServer.ServerUri; + var isMain = string.Equals(serverName, ApiController.SnowcloakServer, 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."); + } + + if (!isMain && selectedServer != _serverConfigurationManager.CurrentServer) + { + if (_uiShared.IconTextButton(FontAwesomeIcon.Trash, "Delete Service") && UiSharedService.CtrlPressed()) + { + _serverConfigurationManager.DeleteServer(selectedServer); + } + _uiShared.DrawHelpText("Hold CTRL to delete this service"); + } + ImGui.EndTabItem(); + } + ImGui.EndTabBar(); + } + } + + private string _uidToAddForIgnore = string.Empty; + private int _selectedEntry = -1; + + private string _uidToAddForIgnoreBlacklist = string.Empty; + private int _selectedEntryBlacklist = -1; + + private void DrawSettingsContent() + { + if (_apiController.ServerState is ServerState.Connected) + { + ImGui.TextUnformatted("Service " + _serverConfigurationManager.CurrentServer!.ServerName + ":"); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGreen, "Available"); + ImGui.SameLine(); + ImGui.TextUnformatted("("); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture)); + ImGui.SameLine(); + ImGui.TextUnformatted("Users Online"); + ImGui.SameLine(); + ImGui.TextUnformatted(")"); + } + ImGui.Separator(); + if (ImGui.BeginTabBar("mainTabBar")) + { + if (ImGui.BeginTabItem("General")) + { + DrawGeneral(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Performance")) + { + DrawPerformance(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Storage")) + { + DrawFileStorageSettings(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Transfers")) + { + DrawCurrentTransfers(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Service Settings")) + { + ImGui.BeginDisabled(_registrationInProgress); + DrawServerConfiguration(); + ImGui.EndTabItem(); + ImGui.EndDisabled(); // _registrationInProgress + } + + if (ImGui.BeginTabItem("Chat")) + { + DrawChatConfig(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Advanced")) + { + DrawAdvanced(); + ImGui.EndTabItem(); + } + + ImGui.EndTabBar(); + } + } + + private void UiSharedService_GposeEnd() + { + IsOpen = _wasOpen; + } + + private void UiSharedService_GposeStart() + { + _wasOpen = IsOpen; + IsOpen = false; + } +} diff --git a/MareSynchronos/UI/StandaloneProfileUi.cs b/MareSynchronos/UI/StandaloneProfileUi.cs new file mode 100644 index 0000000..707e936 --- /dev/null +++ b/MareSynchronos/UI/StandaloneProfileUi.cs @@ -0,0 +1,167 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Interface.Utility; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using Microsoft.Extensions.Logging; +using System.Numerics; + +namespace MareSynchronos.UI; + +public class StandaloneProfileUi : WindowMediatorSubscriberBase +{ + private readonly MareProfileManager _mareProfileManager; + private readonly PairManager _pairManager; + private readonly ServerConfigurationManager _serverManager; + private readonly UiSharedService _uiSharedService; + private bool _adjustedForScrollBars = false; + private byte[] _lastProfilePicture = []; + private IDalamudTextureWrap? _textureWrap; + + public StandaloneProfileUi(ILogger logger, MareMediator mediator, UiSharedService uiBuilder, + ServerConfigurationManager serverManager, MareProfileManager mareProfileManager, PairManager pairManager, Pair pair, + PerformanceCollectorService performanceCollector) + : base(logger, mediator, "Profile of " + pair.UserData.AliasOrUID + "##SnowcloakSyncStandaloneProfileUI" + pair.UserData.AliasOrUID, performanceCollector) + { + _uiSharedService = uiBuilder; + _serverManager = serverManager; + _mareProfileManager = mareProfileManager; + Pair = pair; + _pairManager = pairManager; + Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize; + + var spacing = ImGui.GetStyle().ItemSpacing; + + Size = new(512 + spacing.X * 3 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 512); + + IsOpen = true; + } + + public Pair Pair { get; init; } + + protected override void DrawInternal() + { + try + { + var spacing = ImGui.GetStyle().ItemSpacing; + + var mareProfile = _mareProfileManager.GetMareProfile(Pair.UserData); + + if (_textureWrap == null || !mareProfile.ImageData.Value.SequenceEqual(_lastProfilePicture)) + { + _textureWrap?.Dispose(); + _lastProfilePicture = mareProfile.ImageData.Value; + _textureWrap = _uiSharedService.LoadImage(_lastProfilePicture); + } + + var drawList = ImGui.GetWindowDrawList(); + var rectMin = drawList.GetClipRectMin(); + var rectMax = drawList.GetClipRectMax(); + var headerSize = ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y; + + using (_uiSharedService.UidFont.Push()) + UiSharedService.ColorText(Pair.UserData.AliasOrUID, UiSharedService.AccentColor); + + var reportButtonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.ExclamationTriangle, "Report Profile"); + ImGui.SameLine(ImGui.GetWindowContentRegionMax().X - reportButtonSize); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.ExclamationTriangle, "Report Profile")) + Mediator.Publish(new OpenReportPopupMessage(Pair)); + + 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(mareProfile.Description, hideTextAfterDoubleHash: false, 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(mareProfile.Description); + } + ImGui.EndChildFrame(); + + ImGui.SetCursorPosY(postDummy); + var note = _serverManager.GetNoteForUid(Pair.UserData.UID); + if (!string.IsNullOrEmpty(note)) + { + UiSharedService.ColorText(note, ImGuiColors.DalamudGrey); + } + string status = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline"); + UiSharedService.ColorText(status, (Pair.IsVisible || Pair.IsOnline) ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed); + if (Pair.IsVisible) + { + ImGui.SameLine(); + ImGui.TextUnformatted($"({Pair.PlayerName})"); + } + if (Pair.UserPair != null) + { + ImGui.TextUnformatted("Directly paired"); + if (Pair.UserPair.OwnPermissions.IsPaused()) + { + ImGui.SameLine(); + UiSharedService.ColorText("You: paused", ImGuiColors.DalamudYellow); + } + if (Pair.UserPair.OtherPermissions.IsPaused()) + { + ImGui.SameLine(); + UiSharedService.ColorText("They: paused", ImGuiColors.DalamudYellow); + } + } + + if (Pair.GroupPair.Any()) + { + ImGui.TextUnformatted("Paired through Syncshells:"); + foreach (var groupPair in Pair.GroupPair.Select(k => k.Key)) + { + var groupNote = _serverManager.GetNoteForGid(groupPair.GID); + var groupName = groupPair.GroupAliasOrGID; + var groupString = string.IsNullOrEmpty(groupNote) ? groupName : $"{groupNote} ({groupName})"; + ImGui.TextUnformatted("- " + groupString); + } + } + + 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)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error during draw tooltip"); + } + } + + public override void OnClose() + { + Mediator.Publish(new RemoveWindowMessage(this)); + } +} \ No newline at end of file diff --git a/MareSynchronos/UI/SyncshellAdminUI.cs b/MareSynchronos/UI/SyncshellAdminUI.cs new file mode 100644 index 0000000..cf87eb9 --- /dev/null +++ b/MareSynchronos/UI/SyncshellAdminUI.cs @@ -0,0 +1,455 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; +using System.Globalization; + +namespace MareSynchronos.UI.Components.Popup; + +public class SyncshellAdminUI : WindowMediatorSubscriberBase +{ + private readonly ApiController _apiController; + private readonly bool _isModerator = false; + private readonly bool _isOwner = false; + private readonly List _oneTimeInvites = []; + private readonly PairManager _pairManager; + private readonly UiSharedService _uiSharedService; + private List _bannedUsers = []; + private int _multiInvites; + private string _newPassword; + private bool _pwChangeSuccess; + private Task? _pruneTestTask; + private Task? _pruneTask; + private int _pruneDays = 14; + + public SyncshellAdminUI(ILogger logger, MareMediator mediator, ApiController apiController, + UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService) + : base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService) + { + GroupFullInfo = groupFullInfo; + _apiController = apiController; + _uiSharedService = uiSharedService; + _pairManager = pairManager; + _isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal); + _isModerator = GroupFullInfo.GroupUserInfo.IsModerator(); + _newPassword = string.Empty; + _multiInvites = 30; + _pwChangeSuccess = true; + IsOpen = true; + SizeConstraints = new WindowSizeConstraints() + { + MinimumSize = new(700, 500), + MaximumSize = new(700, 2000), + }; + } + + public GroupFullInfoDto GroupFullInfo { get; private set; } + + protected override void DrawInternal() + { + if (!_isModerator && !_isOwner) return; + + GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group]; + + using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID); + + using (_uiSharedService.UidFont.Push()) + ImGui.TextUnformatted(GroupFullInfo.GroupAliasOrGID + " Administrative Panel"); + + ImGui.Separator(); + var perm = GroupFullInfo.GroupPermissions; + + using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID); + + if (tabbar) + { + var inviteTab = ImRaii.TabItem("Invites"); + if (inviteTab) + { + bool isInvitesDisabled = perm.IsDisableInvites(); + + if (_uiSharedService.IconTextButton(isInvitesDisabled ? FontAwesomeIcon.Unlock : FontAwesomeIcon.Lock, + isInvitesDisabled ? "Unlock Syncshell" : "Lock Syncshell")) + { + perm.SetDisableInvites(!isInvitesDisabled); + _ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm)); + } + + ImGuiHelpers.ScaledDummy(2f); + + UiSharedService.TextWrapped("One-time invites work as single-use passwords. Use those if you do not want to distribute your Syncshell password."); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Envelope, "Single one-time invite")) + { + ImGui.SetClipboardText(_apiController.GroupCreateTempInvite(new(GroupFullInfo.Group), 1).Result.FirstOrDefault() ?? string.Empty); + } + UiSharedService.AttachToolTip("Creates a single-use password for joining the syncshell which is valid for 24h and copies it to the clipboard."); + ImGui.InputInt("##amountofinvites", ref _multiInvites); + ImGui.SameLine(); + using (ImRaii.Disabled(_multiInvites <= 1 || _multiInvites > 100)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Envelope, "Generate " + _multiInvites + " one-time invites")) + { + _oneTimeInvites.AddRange(_apiController.GroupCreateTempInvite(new(GroupFullInfo.Group), _multiInvites).Result); + } + } + + if (_oneTimeInvites.Any()) + { + var invites = string.Join(Environment.NewLine, _oneTimeInvites); + ImGui.InputTextMultiline("Generated Multi Invites", ref invites, 5000, new(0, 0), ImGuiInputTextFlags.ReadOnly); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Copy, "Copy Invites to clipboard")) + { + ImGui.SetClipboardText(invites); + } + } + } + inviteTab.Dispose(); + + var mgmtTab = ImRaii.TabItem("User Management"); + if (mgmtTab) + { + var userNode = ImRaii.TreeNode("User List & Administration"); + if (userNode) + { + if (!_pairManager.GroupPairs.TryGetValue(GroupFullInfo, out var pairs)) + { + UiSharedService.ColorTextWrapped("No users found in this Syncshell", ImGuiColors.DalamudYellow); + } + else + { + using var table = ImRaii.Table("userList#" + GroupFullInfo.Group.GID, 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollY); + if (table) + { + ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 3); + ImGui.TableSetupColumn("Online/Name", ImGuiTableColumnFlags.None, 2); + ImGui.TableSetupColumn("Flags", ImGuiTableColumnFlags.None, 1); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 2); + ImGui.TableHeadersRow(); + + var groupedPairs = new Dictionary(pairs.Select(p => new KeyValuePair(p, + p.GroupPair.TryGetValue(GroupFullInfo, out GroupPairFullInfoDto? value) ? value.GroupPairStatusInfo : null))); + + foreach (var pair in groupedPairs.OrderBy(p => + { + if (p.Value == null) return 10; + if (p.Value.Value.IsModerator()) return 0; + if (p.Value.Value.IsPinned()) return 1; + return 10; + }).ThenBy(p => p.Key.GetNote() ?? p.Key.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase)) + { + using var tableId = ImRaii.PushId("userTable_" + pair.Key.UserData.UID); + + 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(); + ImGui.TextUnformatted(text); + + ImGui.TableNextColumn(); // online/name + string onlineText = pair.Key.IsOnline ? "Online" : "Offline"; + string? name = pair.Key.GetNoteOrName(); + if (!string.IsNullOrEmpty(name)) + { + onlineText += " (" + name + ")"; + } + var boolcolor = UiSharedService.GetBoolColor(pair.Key.IsOnline); + ImGui.AlignTextToFramePadding(); + UiSharedService.ColorText(onlineText, boolcolor); + + ImGui.TableNextColumn(); // special flags + if (pair.Value != null && (pair.Value.Value.IsModerator() || pair.Value.Value.IsPinned())) + { + if (pair.Value.Value.IsModerator()) + { + _uiSharedService.IconText(FontAwesomeIcon.UserShield); + UiSharedService.AttachToolTip("Moderator"); + } + if (pair.Value.Value.IsPinned()) + { + _uiSharedService.IconText(FontAwesomeIcon.Thumbtack); + UiSharedService.AttachToolTip("Pinned"); + } + } + else + { + _uiSharedService.IconText(FontAwesomeIcon.None); + } + + ImGui.TableNextColumn(); // actions + if (_isOwner) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.UserShield)) + { + GroupUserInfo userInfo = pair.Value ?? GroupUserInfo.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 (_isOwner || (pair.Value == null || (pair.Value != null && !pair.Value.Value.IsModerator()))) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Thumbtack)) + { + GroupUserInfo userInfo = pair.Value ?? GroupUserInfo.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.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.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"); + } + } + } + } + } + userNode.Dispose(); + var clearNode = ImRaii.TreeNode("Mass Cleanup"); + if (clearNode) + { + 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"); + + 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 users that have not logged in the past {_pruneDays} days." + + Environment.NewLine + "You will be able to review the amount of inactive users before executing the prune." + + UiSharedService.TooltipSeparator + "Note: pruning excludes pinned users and moderators of this Syncshell."); + ImGui.SameLine(); + ImGui.SetNextItemWidth(150); + _uiSharedService.DrawCombo("Days of inactivity", [7, 14, 30, 90], (count) => + { + return count + " days"; + }, + (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 in the past {_pruneDays} days."); + 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."); + } + } + } + clearNode.Dispose(); + + var banNode = ImRaii.TreeNode("User Bans"); + if (banNode) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server")) + { + _bannedUsers = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group)).Result; + } + + if (ImGui.BeginTable("bannedusertable" + GroupFullInfo.GID, 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollY)) + { + 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 pushId = ImRaii.PushId(bannedUser.UID); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Check, "Unban")) + { + _ = Task.Run(async () => await _apiController.GroupUnbanUser(bannedUser).ConfigureAwait(false)); + _bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal)); + } + } + + ImGui.EndTable(); + } + } + banNode.Dispose(); + } + mgmtTab.Dispose(); + + var permissionTab = ImRaii.TabItem("Permissions"); + if (permissionTab) + { + bool isDisableAnimations = perm.IsDisableAnimations(); + bool isDisableSounds = perm.IsDisableSounds(); + bool isDisableVfx = perm.IsDisableVFX(); + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Sound Sync"); + _uiSharedService.BooleanToColoredIcon(!isDisableSounds); + ImGui.SameLine(230); + if (_uiSharedService.IconTextButton(isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute, + isDisableSounds ? "Enable sound sync" : "Disable sound sync")) + { + perm.SetDisableSounds(!perm.IsDisableSounds()); + _ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm)); + } + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Animation Sync"); + _uiSharedService.BooleanToColoredIcon(!isDisableAnimations); + ImGui.SameLine(230); + if (_uiSharedService.IconTextButton(isDisableAnimations ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop, + isDisableAnimations ? "Enable animation sync" : "Disable animation sync")) + { + perm.SetDisableAnimations(!perm.IsDisableAnimations()); + _ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm)); + } + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("VFX Sync"); + _uiSharedService.BooleanToColoredIcon(!isDisableVfx); + ImGui.SameLine(230); + if (_uiSharedService.IconTextButton(isDisableVfx ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle, + isDisableVfx ? "Enable VFX sync" : "Disable VFX sync")) + { + perm.SetDisableVFX(!perm.IsDisableVFX()); + _ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm)); + } + } + permissionTab.Dispose(); + + if (_isOwner) + { + var ownerTab = ImRaii.TabItem("Owner Settings"); + if (ownerTab) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("New Password"); + var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.Passport, "Change Password"); + var textSize = ImGui.CalcTextSize("New Password").X; + var spacing = ImGui.GetStyle().ItemSpacing.X; + + ImGui.SameLine(); + ImGui.SetNextItemWidth(availableWidth - buttonSize - textSize - spacing * 2); + ImGui.InputTextWithHint("##changepw", "Min 10 characters", ref _newPassword, 50); + ImGui.SameLine(); + using (ImRaii.Disabled(_newPassword.Length < 10)) + { + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Passport, "Change Password")) + { + _pwChangeSuccess = _apiController.GroupChangePassword(new GroupPasswordDto(GroupFullInfo.Group, _newPassword)).Result; + _newPassword = string.Empty; + } + } + UiSharedService.AttachToolTip("Password requires to be at least 10 characters long. This action is irreversible."); + + if (!_pwChangeSuccess) + { + UiSharedService.ColorTextWrapped("Failed to change the password. Password requires to be at least 10 characters long.", ImGuiColors.DalamudYellow); + } + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Delete Syncshell") && UiSharedService.CtrlPressed() && UiSharedService.ShiftPressed()) + { + IsOpen = false; + _ = _apiController.GroupDelete(new(GroupFullInfo.Group)); + } + UiSharedService.AttachToolTip("Hold CTRL and Shift and click to delete this Syncshell." + Environment.NewLine + "WARNING: this action is irreversible."); + } + ownerTab.Dispose(); + } + } + } + + public override void OnClose() + { + Mediator.Publish(new RemoveWindowMessage(this)); + } +} diff --git a/MareSynchronos/UI/UISharedService.cs b/MareSynchronos/UI/UISharedService.cs new file mode 100644 index 0000000..ac9e9b8 --- /dev/null +++ b/MareSynchronos/UI/UISharedService.cs @@ -0,0 +1,1015 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using Dalamud.Utility; +using MareSynchronos.FileCache; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.WebAPI; +using Microsoft.Extensions.Logging; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; + +namespace MareSynchronos.UI; + +public partial class UiSharedService : DisposableMediatorSubscriberBase +{ + public const string TooltipSeparator = "--SEP--"; + public static string DoubleNewLine => Environment.NewLine + Environment.NewLine; + + public static readonly ImGuiWindowFlags PopupWindowFlags = ImGuiWindowFlags.NoResize | + ImGuiWindowFlags.NoScrollbar | + ImGuiWindowFlags.NoScrollWithMouse; + + public static Vector4 AccentColor { get; set; } = ImGuiColors.DalamudYellow; + + public readonly FileDialogManager FileDialogManager; + + private const string _notesEnd = "##MARE_SYNCHRONOS_USER_NOTES_END##"; + + private const string _notesStart = "##MARE_SYNCHRONOS_USER_NOTES_START##"; + + private readonly ApiController _apiController; + + private readonly CacheMonitor _cacheMonitor; + + private readonly MareConfigService _configService; + + private readonly DalamudUtilService _dalamudUtil; + private readonly IpcManager _ipcManager; + private readonly IDalamudPluginInterface _pluginInterface; + private readonly ITextureProvider _textureProvider; + private readonly Dictionary _selectedComboItems = new(StringComparer.Ordinal); + private readonly ServerConfigurationManager _serverConfigurationManager; + private bool _cacheDirectoryHasOtherFilesThanCache = false; + + private bool _cacheDirectoryIsValidPath = true; + + private bool _customizePlusExists = false; + + private string _customServerName = ""; + + private string _customServerUri = ""; + + private bool _glamourerExists = false; + + private bool _heelsExists = false; + + private bool _honorificExists = false; + private bool _isDirectoryWritable = false; + private bool _isOneDrive = false; + private bool _isPenumbraDirectory = false; + private bool _moodlesExists = false; + private bool _penumbraExists = false; + private bool _petNamesExists = false; + private bool _brioExists = false; + + private int _serverSelectionIndex = -1; + + public UiSharedService(ILogger logger, IpcManager ipcManager, ApiController apiController, + CacheMonitor cacheMonitor, FileDialogManager fileDialogManager, + MareConfigService configService, DalamudUtilService dalamudUtil, IDalamudPluginInterface pluginInterface, + ITextureProvider textureProvider, + ServerConfigurationManager serverManager, MareMediator mediator) : base(logger, mediator) + { + _ipcManager = ipcManager; + _apiController = apiController; + _cacheMonitor = cacheMonitor; + FileDialogManager = fileDialogManager; + _configService = configService; + _dalamudUtil = dalamudUtil; + _pluginInterface = pluginInterface; + _textureProvider = textureProvider; + _serverConfigurationManager = serverManager; + + _isDirectoryWritable = IsDirectoryWritable(_configService.Current.CacheFolder); + + Mediator.Subscribe(this, (_) => + { + _penumbraExists = _ipcManager.Penumbra.APIAvailable; + _glamourerExists = _ipcManager.Glamourer.APIAvailable; + _customizePlusExists = _ipcManager.CustomizePlus.APIAvailable; + _heelsExists = _ipcManager.Heels.APIAvailable; + _honorificExists = _ipcManager.Honorific.APIAvailable; + _petNamesExists = _ipcManager.PetNames.APIAvailable; + _moodlesExists = _ipcManager.Moodles.APIAvailable; + _brioExists = _ipcManager.Brio.APIAvailable; + }); + + UidFont = _pluginInterface.UiBuilder.FontAtlas.NewDelegateFontHandle(e => + { + e.OnPreBuild(tk => tk.AddDalamudAssetFont(Dalamud.DalamudAsset.NotoSansJpMedium, new() + { + SizePx = 35, + GlyphRanges = [0x20, 0x7E, 0] + })); + }); + GameFont = _pluginInterface.UiBuilder.FontAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.Axis12)); + IconFont = _pluginInterface.UiBuilder.IconFontFixedWidthHandle; + } + + public ApiController ApiController => _apiController; + + public bool EditTrackerPosition { get; set; } + + public IFontHandle GameFont { get; init; } + public bool HasValidPenumbraModPath => !(_ipcManager.Penumbra.ModDirectory ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.Penumbra.ModDirectory); + + public IFontHandle IconFont { get; init; } + public bool IsInGpose => _dalamudUtil.IsInGpose; + + public string PlayerName => _dalamudUtil.GetPlayerName(); + + public IFontHandle UidFont { get; init; } + public Dictionary WorldData => _dalamudUtil.WorldData.Value; + + public uint WorldId => _dalamudUtil.GetHomeWorldId(); + + public static void AttachToolTip(string text) + { + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35f); + if (text.Contains(TooltipSeparator, StringComparison.Ordinal)) + { + var splitText = text.Split(TooltipSeparator, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < splitText.Length; i++) + { + ImGui.TextUnformatted(splitText[i]); + if (i != splitText.Length - 1) ImGui.Separator(); + } + } + else + { + ImGui.TextUnformatted(text); + } + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + } + } + + public static string ByteToString(long bytes, bool addSuffix = true) + { + _ = addSuffix; + double dblSByte = bytes / 1048576.0; + if (dblSByte > 0.0 && dblSByte < 0.01) + dblSByte = 0.01; + return $"{dblSByte:0.00} MiB"; + } + + public static string TrisToString(long tris) + { + return tris > 1000 ? $"{tris / 1000.0:0.0}k" : $"{tris}"; + } + + public static void CenterNextWindow(float width, float height, ImGuiCond cond = ImGuiCond.None) + { + var center = ImGui.GetMainViewport().GetCenter(); + ImGui.SetNextWindowPos(new Vector2(center.X - width / 2, center.Y - height / 2), cond); + } + + public static uint Color(byte r, byte g, byte b, byte a) + { uint ret = a; ret <<= 8; ret += b; ret <<= 8; ret += g; ret <<= 8; ret += r; return ret; } + + public static uint Color(Vector4 color) + { + uint ret = (byte)(color.W * 255); + ret <<= 8; + ret += (byte)(color.Z * 255); + ret <<= 8; + ret += (byte)(color.Y * 255); + ret <<= 8; + ret += (byte)(color.X * 255); + return ret; + } + + public static void ColorText(string text, Vector4 color) + { + using var raiicolor = ImRaii.PushColor(ImGuiCol.Text, color); + ImGui.TextUnformatted(text); + } + + public static void ColorTextWrapped(string text, Vector4 color, float wrapPos = 0) + { + using var raiicolor = ImRaii.PushColor(ImGuiCol.Text, color); + TextWrapped(text, wrapPos); + } + + public static bool CtrlPressed() => (GetKeyState(0xA2) & 0x8000) != 0 || (GetKeyState(0xA3) & 0x8000) != 0; + + public static void DrawGrouped(Action imguiDrawAction, float rounding = 5f, float? expectedWidth = null) + { + var cursorPos = ImGui.GetCursorPos(); + using (ImRaii.Group()) + { + if (expectedWidth != null) + { + ImGui.Dummy(new(expectedWidth.Value, 0)); + ImGui.SetCursorPos(cursorPos); + } + + imguiDrawAction.Invoke(); + } + + ImGui.GetWindowDrawList().AddRect( + ImGui.GetItemRectMin() - ImGui.GetStyle().ItemInnerSpacing, + ImGui.GetItemRectMax() + ImGui.GetStyle().ItemInnerSpacing, + Color(ImGuiColors.DalamudGrey2), rounding); + } + + public static void DrawGroupedCenteredColorText(string text, Vector4 color, float? maxWidth = null) + { + var availWidth = ImGui.GetContentRegionAvail().X; + var textWidth = ImGui.CalcTextSize(text, hideTextAfterDoubleHash: false, availWidth).X; + if (maxWidth != null && textWidth > maxWidth * ImGuiHelpers.GlobalScale) textWidth = maxWidth.Value * ImGuiHelpers.GlobalScale; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (availWidth / 2f) - (textWidth / 2f)); + DrawGrouped(() => + { + ColorTextWrapped(text, color, ImGui.GetCursorPosX() + textWidth); + }, expectedWidth: maxWidth == null ? null : maxWidth * ImGuiHelpers.GlobalScale); + } + + public static void DrawOutlinedFont(string text, Vector4 fontColor, Vector4 outlineColor, int thickness) + { + var original = ImGui.GetCursorPos(); + + using (ImRaii.PushColor(ImGuiCol.Text, outlineColor)) + { + ImGui.SetCursorPos(original with { Y = original.Y - thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X - thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { Y = original.Y + thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X + thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X - thickness, Y = original.Y - thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X + thickness, Y = original.Y + thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X - thickness, Y = original.Y + thickness }); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original with { X = original.X + thickness, Y = original.Y - thickness }); + ImGui.TextUnformatted(text); + } + + using (ImRaii.PushColor(ImGuiCol.Text, fontColor)) + { + ImGui.SetCursorPos(original); + ImGui.TextUnformatted(text); + ImGui.SetCursorPos(original); + ImGui.TextUnformatted(text); + } + } + + public static void DrawOutlinedFont(ImDrawListPtr drawList, string text, Vector2 textPos, uint fontColor, uint outlineColor, int thickness) + { + drawList.AddText(textPos with { Y = textPos.Y - thickness }, + outlineColor, text); + drawList.AddText(textPos with { X = textPos.X - thickness }, + outlineColor, text); + drawList.AddText(textPos with { Y = textPos.Y + thickness }, + outlineColor, text); + drawList.AddText(textPos with { X = textPos.X + thickness }, + outlineColor, text); + drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y - thickness), + outlineColor, text); + drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y + thickness), + outlineColor, text); + drawList.AddText(new Vector2(textPos.X - thickness, textPos.Y + thickness), + outlineColor, text); + drawList.AddText(new Vector2(textPos.X + thickness, textPos.Y - thickness), + outlineColor, text); + + drawList.AddText(textPos, fontColor, text); + drawList.AddText(textPos, fontColor, text); + } + + public static void DrawTree(string leafName, Action drawOnOpened, ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.None) + { + using var tree = ImRaii.TreeNode(leafName, flags); + if (tree) + { + drawOnOpened(); + } + } + + public static Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; + + public float GetIconTextButtonSize(FontAwesomeIcon icon, string text) + { + Vector2 vector; + using (IconFont.Push()) + vector = ImGui.CalcTextSize(icon.ToIconString()); + + Vector2 vector2 = ImGui.CalcTextSize(text); + float num = 3f * ImGuiHelpers.GlobalScale; + return vector.X + vector2.X + ImGui.GetStyle().FramePadding.X * 2f + num; + } + + public static Vector2 GetIconSize(FontAwesomeIcon icon) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + var iconSize = ImGui.CalcTextSize(icon.ToIconString()); + return iconSize; + } + + public static string GetNotes(List pairs) + { + StringBuilder sb = new(); + sb.AppendLine(_notesStart); + foreach (var entry in pairs) + { + var note = entry.GetNote(); + if (note.IsNullOrEmpty()) continue; + + sb.Append(entry.UserData.UID).Append(":\"").Append(entry.GetNote()).AppendLine("\""); + } + sb.AppendLine(_notesEnd); + + return sb.ToString(); + } + + public static float GetWindowContentRegionWidth() + { + return ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; + } + + public static float GetWindowContentRegionHeight() + { + return ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y; + } + + public bool IconButton(FontAwesomeIcon icon, float? height = null) + { + string text = icon.ToIconString(); + + ImGui.PushID(text); + Vector2 vector; + using (IconFont.Push()) + vector = ImGui.CalcTextSize(text); + ImDrawListPtr windowDrawList = ImGui.GetWindowDrawList(); + Vector2 cursorScreenPos = ImGui.GetCursorScreenPos(); + float x = vector.X + ImGui.GetStyle().FramePadding.X * 2f; + float frameHeight = height ?? ImGui.GetFrameHeight(); + bool result = ImGui.Button(string.Empty, new Vector2(x, frameHeight)); + Vector2 pos = new Vector2(cursorScreenPos.X + ImGui.GetStyle().FramePadding.X, + cursorScreenPos.Y + (height ?? ImGui.GetFrameHeight()) / 2f - (vector.Y / 2f)); + using (IconFont.Push()) + windowDrawList.AddText(pos, ImGui.GetColorU32(ImGuiCol.Text), text); + ImGui.PopID(); + + return result; + } + + private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null) + { + int num = 0; + if (defaultColor.HasValue) + { + ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value); + num++; + } + + ImGui.PushID(text); + Vector2 vector; + using (IconFont.Push()) + vector = ImGui.CalcTextSize(icon.ToIconString()); + Vector2 vector2 = ImGui.CalcTextSize(text); + ImDrawListPtr windowDrawList = ImGui.GetWindowDrawList(); + Vector2 cursorScreenPos = ImGui.GetCursorScreenPos(); + float num2 = 3f * ImGuiHelpers.GlobalScale; + float x = width ?? vector.X + vector2.X + ImGui.GetStyle().FramePadding.X * 2f + num2; + float frameHeight = ImGui.GetFrameHeight(); + bool result = ImGui.Button(string.Empty, new Vector2(x, frameHeight)); + Vector2 pos = new Vector2(cursorScreenPos.X + ImGui.GetStyle().FramePadding.X, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y); + using (IconFont.Push()) + 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); + ImGui.PopID(); + if (num > 0) + { + ImGui.PopStyleColor(num); + } + + return result; + } + + public bool IconTextButton(FontAwesomeIcon icon, string text, float? width = null, bool isInPopup = false) + { + return IconTextButtonInternal(icon, text, + isInPopup ? ColorHelpers.RgbaUintToVector4(ImGui.GetColorU32(ImGuiCol.PopupBg)) : null, + width <= 0 ? null : width); + } + + public static bool IsDirectoryWritable(string dirPath, bool throwIfFails = false) + { + try + { + using FileStream fs = File.Create( + Path.Combine( + dirPath, + Path.GetRandomFileName() + ), + 1, + FileOptions.DeleteOnClose); + return true; + } + catch + { + if (throwIfFails) + throw; + + return false; + } + } + + public static void SetScaledWindowSize(float width, bool centerWindow = true) + { + var newLineHeight = ImGui.GetCursorPosY(); + ImGui.NewLine(); + newLineHeight = ImGui.GetCursorPosY() - newLineHeight; + var y = ImGui.GetCursorPos().Y + ImGui.GetWindowContentRegionMin().Y - newLineHeight * 2 - ImGui.GetStyle().ItemSpacing.Y; + + SetScaledWindowSize(width, y, centerWindow, scaledHeight: true); + } + + public static void SetScaledWindowSize(float width, float height, bool centerWindow = true, bool scaledHeight = false) + { + ImGui.SameLine(); + var x = width * ImGuiHelpers.GlobalScale; + var y = scaledHeight ? height : height * ImGuiHelpers.GlobalScale; + + if (centerWindow) + { + CenterWindow(x, y); + } + + ImGui.SetWindowSize(new Vector2(x, y)); + } + + public static bool ShiftPressed() => (GetKeyState(0xA1) & 0x8000) != 0 || (GetKeyState(0xA0) & 0x8000) != 0; + + public static void TextWrapped(string text, float wrapPos = 0) + { + ImGui.PushTextWrapPos(wrapPos); + ImGui.TextUnformatted(text); + ImGui.PopTextWrapPos(); + } + + public static Vector4 UploadColor((long, long) data) => data.Item1 == 0 ? ImGuiColors.DalamudGrey : + data.Item1 == data.Item2 ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudYellow; + + public bool ApplyNotesFromClipboard(string notes, bool overwrite) + { + var splitNotes = notes.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).ToList(); + var splitNotesStart = splitNotes.FirstOrDefault(); + var splitNotesEnd = splitNotes.LastOrDefault(); + if (!string.Equals(splitNotesStart, _notesStart, StringComparison.Ordinal) || !string.Equals(splitNotesEnd, _notesEnd, StringComparison.Ordinal)) + { + return false; + } + + splitNotes.RemoveAll(n => string.Equals(n, _notesStart, StringComparison.Ordinal) || string.Equals(n, _notesEnd, StringComparison.Ordinal)); + + foreach (var note in splitNotes) + { + try + { + var splittedEntry = note.Split(":", 2, StringSplitOptions.RemoveEmptyEntries); + var uid = splittedEntry[0]; + var comment = splittedEntry[1].Trim('"'); + if (_serverConfigurationManager.GetNoteForUid(uid) != null && !overwrite) continue; + _serverConfigurationManager.SetNoteForUid(uid, comment); + } + catch + { + Logger.LogWarning("Could not parse {note}", note); + } + } + + _serverConfigurationManager.SaveNotes(); + + return true; + } + + public void BigText(string text, Vector4? color = null) + { + FontText(text, UidFont, color); + } + + public void BooleanToColoredIcon(bool value, bool inline = true) + { + using var colorgreen = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen, value); + using var colorred = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !value); + + if (inline) ImGui.SameLine(); + + if (value) + { + IconText(FontAwesomeIcon.Check); + } + else + { + IconText(FontAwesomeIcon.Times); + } + } + + public void DrawCacheDirectorySetting() + { + ColorTextWrapped("Note: The storage folder should be somewhere close to root (i.e. C:\\SnowcloakStorage) in a new empty folder. DO NOT point this to your game folder. DO NOT point this to your Penumbra folder.", ImGuiColors.DalamudYellow); + var cacheDirectory = _configService.Current.CacheFolder; + ImGui.SetNextItemWidth(400 * ImGuiHelpers.GlobalScale); + ImGui.InputText("Storage Folder##cache", ref cacheDirectory, 255, ImGuiInputTextFlags.ReadOnly); + + ImGui.SameLine(); + using (ImRaii.Disabled(_cacheMonitor.MareWatcher != null)) + { + if (IconButton(FontAwesomeIcon.Folder)) + { + FileDialogManager.OpenFolderDialog("Pick Snowcloak Storage Folder", (success, path) => + { + if (!success) return; + + _isOneDrive = path.Contains("onedrive", StringComparison.OrdinalIgnoreCase); + _isPenumbraDirectory = string.Equals(path.ToLowerInvariant(), _ipcManager.Penumbra.ModDirectory?.ToLowerInvariant(), StringComparison.Ordinal); + _isDirectoryWritable = IsDirectoryWritable(path); + _cacheDirectoryHasOtherFilesThanCache = false; + var cacheDirFiles = Directory.GetFiles(path, "*", SearchOption.AllDirectories); + var cacheSubDirs = Directory.GetDirectories(path); + + _cacheDirectoryHasOtherFilesThanCache = cacheDirFiles.Any(f => + Path.GetFileNameWithoutExtension(f).Length != 40 + && !Path.GetExtension(f).Equals("tmp", StringComparison.OrdinalIgnoreCase) + && !Path.GetExtension(f).Equals("blk", StringComparison.OrdinalIgnoreCase) + ); + + if (!_cacheDirectoryHasOtherFilesThanCache + && cacheSubDirs.Select(f => Path.GetFileName(Path.TrimEndingDirectorySeparator(f))).Any(f => + !f.Equals("subst", StringComparison.OrdinalIgnoreCase) + )) + _cacheDirectoryHasOtherFilesThanCache = true; + + _cacheDirectoryIsValidPath = PathRegex().IsMatch(path); + + if (!string.IsNullOrEmpty(path) + && Directory.Exists(path) + && _isDirectoryWritable + && !_isPenumbraDirectory + && !_isOneDrive + && !_cacheDirectoryHasOtherFilesThanCache + && _cacheDirectoryIsValidPath) + { + _configService.Current.CacheFolder = path; + _configService.Save(); + _cacheMonitor.StartMareWatcher(path); + _cacheMonitor.InvokeScan(); + } + }, _dalamudUtil.IsWine ? @"Z:\" : @"C:\"); + } + } + if (_cacheMonitor.MareWatcher != null) + { + AttachToolTip("Stop the Monitoring before changing the Storage folder. As long as monitoring is active, you cannot change the Storage folder location."); + } + + if (_isPenumbraDirectory) + { + ColorTextWrapped("Do not point the storage path directly to the Penumbra directory. If necessary, make a subfolder in it.", ImGuiColors.DalamudRed); + } + else if (_isOneDrive) + { + ColorTextWrapped("Do not point the storage path to a folder in OneDrive. Do not use OneDrive folders for any Mod related functionality.", ImGuiColors.DalamudRed); + } + else if (!_isDirectoryWritable) + { + ColorTextWrapped("The folder you selected does not exist or cannot be written to. Please provide a valid path.", ImGuiColors.DalamudRed); + } + else if (_cacheDirectoryHasOtherFilesThanCache) + { + ColorTextWrapped("Your selected directory has files or directories inside that are not Snowcloak related. Use an empty directory or a previous storage directory only.", ImGuiColors.DalamudRed); + } + else if (!_cacheDirectoryIsValidPath) + { + ColorTextWrapped("Your selected directory contains illegal characters unreadable by FFXIV. " + + "Restrict yourself to latin letters (A-Z), underscores (_), dashes (-) and arabic numbers (0-9).", ImGuiColors.DalamudRed); + } + + float maxCacheSize = (float)_configService.Current.MaxLocalCacheInGiB; + ImGui.SetNextItemWidth(400 * ImGuiHelpers.GlobalScale); + if (ImGui.SliderFloat("Maximum Storage Size", ref maxCacheSize, 1f, 200f, "%.2f GiB")) + { + _configService.Current.MaxLocalCacheInGiB = maxCacheSize; + _configService.Save(); + } + DrawHelpText("The storage is automatically governed by Snowcloak. It will clear itself automatically once it reaches the set capacity by removing the oldest unused files. You typically do not need to clear it yourself."); + } + + public T? DrawCombo(string comboName, IEnumerable comboItems, Func toName, + Action? onSelected = null, T? initialSelectedItem = default) + { + if (!comboItems.Any()) return default; + + if (!_selectedComboItems.TryGetValue(comboName, out var selectedItem) && selectedItem == null) + { + if (!EqualityComparer.Default.Equals(initialSelectedItem, default)) + { + selectedItem = initialSelectedItem; + _selectedComboItems[comboName] = selectedItem!; + if (!EqualityComparer.Default.Equals(initialSelectedItem, default)) + onSelected?.Invoke(initialSelectedItem); + } + else + { + selectedItem = comboItems.First(); + _selectedComboItems[comboName] = selectedItem!; + } + } + + if (ImGui.BeginCombo(comboName, toName((T)selectedItem!))) + { + foreach (var item in comboItems) + { + bool isSelected = EqualityComparer.Default.Equals(item, (T?)selectedItem); + if (ImGui.Selectable(toName(item), isSelected)) + { + _selectedComboItems[comboName] = item!; + onSelected?.Invoke(item!); + } + } + + ImGui.EndCombo(); + } + + return (T)_selectedComboItems[comboName]; + } + + public T? DrawColorCombo(string comboName, IEnumerable comboItems, Func toEntry, + Action? onSelected = null, T? initialSelectedItem = default) + { + if (!comboItems.Any()) return default; + + if (!_selectedComboItems.TryGetValue(comboName, out var selectedItem) && selectedItem == null) + { + if (!EqualityComparer.Default.Equals(initialSelectedItem, default)) + { + selectedItem = initialSelectedItem; + _selectedComboItems[comboName] = selectedItem!; + if (!EqualityComparer.Default.Equals(initialSelectedItem, default)) + onSelected?.Invoke(initialSelectedItem); + } + else + { + selectedItem = comboItems.First(); + _selectedComboItems[comboName] = selectedItem!; + } + } + + var entry = toEntry((T)selectedItem!); + ImGui.PushStyleColor(ImGuiCol.Text, ColorHelpers.RgbaUintToVector4(ColorHelpers.SwapEndianness(entry.Color))); + if (ImGui.BeginCombo(comboName, entry.Name)) + { + foreach (var item in comboItems) + { + entry = toEntry(item); + ImGui.PushStyleColor(ImGuiCol.Text, ColorHelpers.RgbaUintToVector4(ColorHelpers.SwapEndianness(entry.Color))); + bool isSelected = EqualityComparer.Default.Equals(item, (T)selectedItem!); + if (ImGui.Selectable(entry.Name, isSelected)) + { + _selectedComboItems[comboName] = item!; + onSelected?.Invoke(item!); + } + ImGui.PopStyleColor(); + } + + ImGui.EndCombo(); + } + ImGui.PopStyleColor(); + + return (T)_selectedComboItems[comboName]; + } + + public void DrawFileScanState() + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("File Scanner Status"); + ImGui.SameLine(); + if (_cacheMonitor.IsScanRunning) + { + ImGui.AlignTextToFramePadding(); + + ImGui.TextUnformatted("Scan is running"); + ImGui.TextUnformatted("Current Progress:"); + ImGui.SameLine(); + ImGui.TextUnformatted(_cacheMonitor.TotalFiles == 1 + ? "Collecting files" + : $"Processing {_cacheMonitor.CurrentFileProgress}/{_cacheMonitor.TotalFilesStorage} from storage ({_cacheMonitor.TotalFiles} scanned in)"); + AttachToolTip("Note: it is possible to have more files in storage than scanned in, " + + "this is due to the scanner normally ignoring those files but the game loading them in and using them on your character, so they get " + + "added to the local storage."); + } + else if (_cacheMonitor.HaltScanLocks.Any(f => f.Value.Value > 0)) + { + ImGui.AlignTextToFramePadding(); + + ImGui.TextUnformatted("Halted (" + string.Join(", ", _cacheMonitor.HaltScanLocks.Where(f => f.Value.Value > 0).Select(locker => locker.Key + ": " + locker.Value.Value)) + ")"); + ImGui.SameLine(); + if (ImGui.Button("Reset halt requests##clearlocks")) + { + _cacheMonitor.ResetLocks(); + } + } + else + { + ImGui.TextUnformatted("Idle"); + if (_configService.Current.InitialScanComplete) + { + ImGui.SameLine(); + if (IconTextButton(FontAwesomeIcon.Play, "Force rescan")) + { + _cacheMonitor.InvokeScan(); + } + } + } + } + public void DrawHelpText(string helpText) + { + ImGui.SameLine(); + IconText(FontAwesomeIcon.QuestionCircle, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + AttachToolTip(helpText); + } + + public bool DrawOtherPluginState(bool intro = false) + { + var check = FontAwesomeIcon.Check; + var cross = FontAwesomeIcon.SquareXmark; + + if (intro) + { + ImGui.SetWindowFontScale(0.8f); + BigText("Mandatory Plugins"); + ImGui.SetWindowFontScale(1.0f); + } + else + { + ImGui.TextUnformatted("Mandatory Plugins:"); + ImGui.SameLine(); + } + + ImGui.TextUnformatted("Penumbra"); + ImGui.SameLine(); + IconText(_penumbraExists ? check : cross, GetBoolColor(_penumbraExists)); + ImGui.SameLine(); + AttachToolTip($"Penumbra is " + (_penumbraExists ? "available and up to date." : "unavailable or not up to date.")); + + ImGui.TextUnformatted("Glamourer"); + ImGui.SameLine(); + IconText(_glamourerExists ? check : cross, GetBoolColor(_glamourerExists)); + AttachToolTip($"Glamourer is " + (_glamourerExists ? "available and up to date." : "unavailable or not up to date.")); + + if (intro) + { + ImGui.SetWindowFontScale(0.8f); + BigText("Optional Addons"); + ImGui.SetWindowFontScale(1.0f); + UiSharedService.TextWrapped("These addons are not required for basic operation, but without them you may not see others as intended."); + } + else + { + ImGui.TextUnformatted("Optional Addons:"); + ImGui.SameLine(); + } + + var alignPos = ImGui.GetCursorPosX(); + + ImGui.TextUnformatted("SimpleHeels"); + ImGui.SameLine(); + IconText(_heelsExists ? check : cross, GetBoolColor(_heelsExists)); + ImGui.SameLine(); + AttachToolTip($"SimpleHeels is " + (_heelsExists ? "available and up to date." : "unavailable or not up to date.")); + ImGui.Spacing(); + + ImGui.SameLine(); + ImGui.TextUnformatted("Customize+"); + ImGui.SameLine(); + IconText(_customizePlusExists ? check : cross, GetBoolColor(_customizePlusExists)); + ImGui.SameLine(); + AttachToolTip($"Customize+ is " + (_customizePlusExists ? "available and up to date." : "unavailable or not up to date.")); + ImGui.Spacing(); + + ImGui.SameLine(); + ImGui.TextUnformatted("Honorific"); + ImGui.SameLine(); + IconText(_honorificExists ? check : cross, GetBoolColor(_honorificExists)); + ImGui.SameLine(); + AttachToolTip($"Honorific is " + (_honorificExists ? "available and up to date." : "unavailable or not up to date.")); + ImGui.Spacing(); + + ImGui.SameLine(); + ImGui.TextUnformatted("PetNicknames"); + ImGui.SameLine(); + IconText(_petNamesExists ? check : cross, GetBoolColor(_petNamesExists)); + ImGui.SameLine(); + AttachToolTip($"PetNicknames is " + (_petNamesExists ? "available and up to date." : "unavailable or not up to date.")); + ImGui.Spacing(); + + ImGui.SetCursorPosX(alignPos); + ImGui.TextUnformatted("Moodles"); + ImGui.SameLine(); + IconText(_moodlesExists ? check : cross, GetBoolColor(_moodlesExists)); + ImGui.SameLine(); + AttachToolTip($"Moodles is " + (_moodlesExists ? "available and up to date." : "unavailable or not up to date.")); + ImGui.Spacing(); + + ImGui.SameLine(); + ImGui.TextUnformatted("Brio"); + ImGui.SameLine(); + IconText(_brioExists ? check : cross, GetBoolColor(_brioExists)); + ImGui.SameLine(); + AttachToolTip($"Brio is " + (_moodlesExists ? "available and up to date." : "unavailable or not up to date.")); + ImGui.Spacing(); + + if (!_penumbraExists || !_glamourerExists) + { + ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Snowcloak."); + return false; + } + + return true; + } + + public int DrawServiceSelection(bool selectOnChange = false, bool intro = false) + { + string[] comboEntries = _serverConfigurationManager.GetServerNames(); + + if (_serverSelectionIndex == -1) + { + _serverSelectionIndex = Array.IndexOf(_serverConfigurationManager.GetServerApiUrls(), _serverConfigurationManager.CurrentApiUrl); + } + if (_serverSelectionIndex == -1 || _serverSelectionIndex >= comboEntries.Length) + { + _serverSelectionIndex = 0; + } + for (int i = 0; i < comboEntries.Length; i++) + { + if (string.Equals(_serverConfigurationManager.CurrentServer?.ServerName, comboEntries[i], StringComparison.OrdinalIgnoreCase)) + comboEntries[i] += " [Current]"; + } + ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); + if (ImGui.BeginCombo("Select Service", comboEntries[_serverSelectionIndex])) + { + for (int i = 0; i < comboEntries.Length; i++) + { + bool isSelected = _serverSelectionIndex == i; + if (ImGui.Selectable(comboEntries[i], isSelected)) + { + _serverSelectionIndex = i; + if (selectOnChange) + { + _serverConfigurationManager.SelectServer(i); + } + } + + if (isSelected) + { + ImGui.SetItemDefaultFocus(); + } + } + + ImGui.EndCombo(); + } + + if (intro) + return _serverSelectionIndex; + + ImGui.SameLine(); + var text = "Connect"; + if (_serverSelectionIndex == _serverConfigurationManager.CurrentServerIndex) text = "Reconnect"; + if (IconTextButton(FontAwesomeIcon.Link, text)) + { + _serverConfigurationManager.SelectServer(_serverSelectionIndex); + _ = _apiController.CreateConnections(); + } + + if (ImGui.TreeNode("Add Custom Service")) + { + ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); + ImGui.InputText("Custom Service URI", ref _customServerUri, 255); + ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); + ImGui.InputText("Custom Service Name", ref _customServerName, 255); + if (IconTextButton(FontAwesomeIcon.Plus, "Add Custom Service") + && !string.IsNullOrEmpty(_customServerUri) + && !string.IsNullOrEmpty(_customServerName)) + { + _serverConfigurationManager.AddServer(new ServerStorage() + { + ServerName = _customServerName, + ServerUri = _customServerUri, + }); + _customServerName = string.Empty; + _customServerUri = string.Empty; + _configService.Save(); + } + ImGui.TreePop(); + } + + return _serverSelectionIndex; + } + + public Vector2 GetIconButtonSize(FontAwesomeIcon icon) + { + using var font = IconFont.Push(); + return ImGuiHelpers.GetButtonSize(icon.ToIconString()); + } + + public Vector2 GetIconData(FontAwesomeIcon icon) + { + using var font = IconFont.Push(); + return ImGui.CalcTextSize(icon.ToIconString()); + } + + public void IconText(FontAwesomeIcon icon, uint color) + { + FontText(icon.ToIconString(), IconFont, color); + } + + public void IconText(FontAwesomeIcon icon, Vector4? color = null) + { + IconText(icon, color == null ? ImGui.GetColorU32(ImGuiCol.Text) : ImGui.GetColorU32(color.Value)); + } + + public IDalamudTextureWrap LoadImage(byte[] imageData) + { + if (imageData.Length == 0) + { + return _textureProvider.CreateEmpty(new() + { + Width = 256, + Height = 256, + DxgiFormat = 3, + Pitch = 1024 + }, cpuRead: false, cpuWrite: false); + } + return _textureProvider.CreateFromImageAsync(imageData).Result; + } + + internal static void DistanceSeparator() + { + ImGuiHelpers.ScaledDummy(5); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(5); + } + + [LibraryImport("user32")] + internal static partial short GetKeyState(int nVirtKey); + + private static void CenterWindow(float width, float height, ImGuiCond cond = ImGuiCond.None) + { + var center = ImGui.GetMainViewport().GetCenter(); + ImGui.SetWindowPos(new Vector2(center.X - width / 2, center.Y - height / 2), cond); + } + + [GeneratedRegex(@"^(?:[a-zA-Z]:\\[\w\s\-\\]+?|\/(?:[\w\s\-\/])+?)$", RegexOptions.ECMAScript, 5000)] + private static partial Regex PathRegex(); + + private void FontText(string text, IFontHandle font, Vector4? color = null) + { + FontText(text, font, color == null ? ImGui.GetColorU32(ImGuiCol.Text) : ImGui.GetColorU32(color.Value)); + } + + private void FontText(string text, IFontHandle font, uint color) + { + using var pushedFont = font.Push(); + using var pushedColor = ImRaii.PushColor(ImGuiCol.Text, color); + ImGui.TextUnformatted(text); + } + + public sealed record IconScaleData(Vector2 IconSize, Vector2 NormalizedIconScale, float OffsetX, float IconScaling); + + protected override void Dispose(bool disposing) + { + if (!disposing) return; + + base.Dispose(disposing); + + UidFont.Dispose(); + GameFont.Dispose(); + } +} diff --git a/MareSynchronos/Utils/ChatUtils.cs b/MareSynchronos/Utils/ChatUtils.cs new file mode 100644 index 0000000..b7a63b0 --- /dev/null +++ b/MareSynchronos/Utils/ChatUtils.cs @@ -0,0 +1,34 @@ +using Dalamud.Game.Text.SeStringHandling.Payloads; +using System.Security.Cryptography; +using System.Text; + +namespace MareSynchronos.Utils; + +public static class ChatUtils +{ + // Based on https://git.anna.lgbt/anna/ExtraChat/src/branch/main/client/ExtraChat/Util/PayloadUtil.cs + // This must store a Guid (16 bytes), as Chat 2 converts the data back to one + + public static RawPayload CreateExtraChatTagPayload(Guid guid) + { + var header = (byte[])[ + 0x02, // Payload.START_BYTE + 0x27, // SeStringChunkType.Interactable + 2 + 16, // remaining length: ExtraChat sends 19 here but I think its an error + 0x20 // Custom ExtraChat InfoType + ]; + + var footer = (byte)0x03; // Payload.END_BYTE + + return new RawPayload([..header, ..guid.ToByteArray(), footer]); + } + + // We have a unique identifier in the form of a GID, which can be consistently mapped to the same GUID + public static RawPayload CreateExtraChatTagPayload(string gid) + { + var gidBytes = UTF8Encoding.UTF8.GetBytes(gid); + var hashedBytes = MD5.HashData(gidBytes); + var guid = new Guid(hashedBytes); + return CreateExtraChatTagPayload(guid); + } +} diff --git a/MareSynchronos/Utils/Crypto.cs b/MareSynchronos/Utils/Crypto.cs new file mode 100644 index 0000000..d029a83 --- /dev/null +++ b/MareSynchronos/Utils/Crypto.cs @@ -0,0 +1,28 @@ +using System.Security.Cryptography; +using System.Text; + +namespace MareSynchronos.Utils; + +public static class Crypto +{ +#pragma warning disable SYSLIB0021 // Type or member is obsolete + + private static readonly SHA256CryptoServiceProvider _sha256CryptoProvider = new(); + + public static string GetFileHash(this string filePath) + { + using SHA1CryptoServiceProvider cryptoProvider = new(); + return BitConverter.ToString(cryptoProvider.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", "", StringComparison.Ordinal); + } + + public static string GetHash256(this string stringToHash) + { + return GetOrComputeHashSHA256(stringToHash); + } + + private static string GetOrComputeHashSHA256(string stringToCompute) + { + return BitConverter.ToString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToCompute))).Replace("-", "", StringComparison.Ordinal); + } +#pragma warning restore SYSLIB0021 // Type or member is obsolete +} \ No newline at end of file diff --git a/MareSynchronos/Utils/HashingStream.cs b/MareSynchronos/Utils/HashingStream.cs new file mode 100644 index 0000000..b03ed31 --- /dev/null +++ b/MareSynchronos/Utils/HashingStream.cs @@ -0,0 +1,80 @@ +using System.Security.Cryptography; + +namespace MareSynchronos.Utils; + +// Calculates the hash of content read or written to a stream +public class HashingStream : Stream +{ + private readonly Stream _stream; + private readonly HashAlgorithm _hashAlgo; + private bool _finished = false; + public bool DisposeUnderlying { get; set; } = true; + + public Stream UnderlyingStream { get => _stream; } + + public HashingStream(Stream underlyingStream, HashAlgorithm hashAlgo) + { + _stream = underlyingStream; + _hashAlgo = hashAlgo; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!DisposeUnderlying) + return; + if (!_finished) + _stream.Dispose(); + _hashAlgo.Dispose(); + } + + public override bool CanRead => _stream.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => _stream.CanWrite; + public override long Length => _stream.Length; + + public override long Position { get => _stream.Position; set => throw new NotSupportedException(); } + + public override void Flush() + { + _stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ObjectDisposedException.ThrowIf(_finished, this); + int n = _stream.Read(buffer, offset, count); + if (n > 0) + _hashAlgo.TransformBlock(buffer, offset, n, buffer, offset); + return n; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + ObjectDisposedException.ThrowIf(_finished, this); + _stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + ObjectDisposedException.ThrowIf(_finished, this); + _stream.Write(buffer, offset, count); + _hashAlgo.TransformBlock(buffer, offset, count, buffer, offset); + } + + public byte[] Finish() + { + if (_finished) + return _hashAlgo.Hash!; + _hashAlgo.TransformFinalBlock(Array.Empty(), 0, 0); + _finished = true; + if (DisposeUnderlying) + _stream.Dispose(); + return _hashAlgo.Hash!; + } +} diff --git a/MareSynchronos/Utils/LimitedStream.cs b/MareSynchronos/Utils/LimitedStream.cs new file mode 100644 index 0000000..0202c07 --- /dev/null +++ b/MareSynchronos/Utils/LimitedStream.cs @@ -0,0 +1,128 @@ +namespace MareSynchronos.Utils; + +// Limits the number of bytes read/written to an underlying stream +public class LimitedStream : Stream +{ + private readonly Stream _stream; + private long _estimatedPosition = 0; + public long MaxPosition { get; private init; } + public bool DisposeUnderlying { get; set; } = true; + + public Stream UnderlyingStream { get => _stream; } + + public LimitedStream(Stream underlyingStream, long byteLimit) + { + _stream = underlyingStream; + try + { + _estimatedPosition = _stream.Position; + } + catch { } + MaxPosition = _estimatedPosition + byteLimit; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!DisposeUnderlying) + return; + _stream.Dispose(); + } + + public override bool CanRead => _stream.CanRead; + public override bool CanSeek => _stream.CanSeek; + public override bool CanWrite => _stream.CanWrite; + public override long Length => _stream.Length; + + public override long Position { get => _stream.Position; set => _stream.Position = _estimatedPosition = value; } + + public override void Flush() + { + _stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int remainder = (int)long.Clamp(MaxPosition - _estimatedPosition, 0, int.MaxValue); + + if (count > remainder) + count = remainder; + + int n = _stream.Read(buffer, offset, count); + _estimatedPosition += n; + return n; + } + + public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int remainder = (int)long.Clamp(MaxPosition - _estimatedPosition, 0, int.MaxValue); + + if (count > remainder) + count = remainder; + +#pragma warning disable CA1835 + int n = await _stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); +#pragma warning restore CA1835 + _estimatedPosition += n; + return n; + } + + public async override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int remainder = (int)long.Clamp(MaxPosition - _estimatedPosition, 0, int.MaxValue); + + if (buffer.Length > remainder) + buffer = buffer[..remainder]; + + int n = await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + _estimatedPosition += n; + return n; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long result = _stream.Seek(offset, origin); + _estimatedPosition = result; + return result; + } + + public override void SetLength(long value) + { + _stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + int remainder = (int)long.Clamp(MaxPosition - _estimatedPosition, 0, int.MaxValue); + + if (count > remainder) + count = remainder; + + _stream.Write(buffer, offset, count); + _estimatedPosition += count; + } + + public async override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int remainder = (int)long.Clamp(MaxPosition - _estimatedPosition, 0, int.MaxValue); + + if (count > remainder) + count = remainder; + +#pragma warning disable CA1835 + await _stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); +#pragma warning restore CA1835 + _estimatedPosition += count; + } + + public async override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + int remainder = (int)long.Clamp(MaxPosition - _estimatedPosition, 0, int.MaxValue); + + if (buffer.Length > remainder) + buffer = buffer[..remainder]; + + await _stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + _estimatedPosition += buffer.Length; + } +} diff --git a/MareSynchronos/Utils/MareInterpolatedStringHandler.cs b/MareSynchronos/Utils/MareInterpolatedStringHandler.cs new file mode 100644 index 0000000..2f96533 --- /dev/null +++ b/MareSynchronos/Utils/MareInterpolatedStringHandler.cs @@ -0,0 +1,27 @@ +using System.Runtime.CompilerServices; +using System.Text; + +namespace MareSynchronos.Utils; + +[InterpolatedStringHandler] +public readonly ref struct MareInterpolatedStringHandler +{ + readonly StringBuilder _logMessageStringbuilder; + + public MareInterpolatedStringHandler(int literalLength, int formattedCount) + { + _logMessageStringbuilder = new StringBuilder(literalLength); + } + + public void AppendLiteral(string s) + { + _logMessageStringbuilder.Append(s); + } + + public void AppendFormatted(T t) + { + _logMessageStringbuilder.Append(t?.ToString()); + } + + public string BuildMessage() => _logMessageStringbuilder.ToString(); +} diff --git a/MareSynchronos/Utils/PngHdr.cs b/MareSynchronos/Utils/PngHdr.cs new file mode 100644 index 0000000..16631b8 --- /dev/null +++ b/MareSynchronos/Utils/PngHdr.cs @@ -0,0 +1,57 @@ +namespace MareSynchronos.Utils; + +public class PngHdr +{ + private static readonly byte[] _magicSignature = [137, 80, 78, 71, 13, 10, 26, 10]; + private static readonly byte[] _IHDR = [(byte)'I', (byte)'H', (byte)'D', (byte)'R']; + public static readonly (int Width, int Height) InvalidSize = (0, 0); + + public static (int Width, int Height) TryExtractDimensions(Stream stream) + { + Span buffer = stackalloc byte[8]; + + try + { + stream.ReadExactly(buffer[..8]); + + // All PNG files start with the same 8 bytes + if (!buffer.SequenceEqual(_magicSignature)) + return InvalidSize; + + stream.ReadExactly(buffer[..8]); + + uint ihdrLength = ReadBigEndianUInt32(buffer[..4]); + + // The next four bytes will be the length of the IHDR section (it should be 13 bytes but we only need 8) + if (ihdrLength < 8) + return InvalidSize; + + // followed by ASCII "IHDR" + if (!buffer[4..].SequenceEqual(_IHDR)) + return InvalidSize; + + stream.ReadExactly(buffer[..8]); + + uint width = ReadBigEndianUInt32(buffer[..4]); + uint height = ReadBigEndianUInt32(buffer[4..8]); + + // Validate the width/height are non-negative and... that's all we care about! + if (width > int.MaxValue || height > int.MaxValue) + return InvalidSize; + + return ((int)width, (int)height); + } + catch (EndOfStreamException) + { + return InvalidSize; + } + } + // Minimal helper for big-endian conversion + private static uint ReadBigEndianUInt32(ReadOnlySpan bytes) + { + return ((uint)bytes[0] << 24) | + ((uint)bytes[1] << 16) | + ((uint)bytes[2] << 8) | + bytes[3]; + } +} \ No newline at end of file diff --git a/MareSynchronos/Utils/RollingList.cs b/MareSynchronos/Utils/RollingList.cs new file mode 100644 index 0000000..977ec68 --- /dev/null +++ b/MareSynchronos/Utils/RollingList.cs @@ -0,0 +1,47 @@ +using System.Collections; + +namespace MareSynchronos.Utils; + +public class RollingList : IEnumerable +{ + private readonly Lock _addLock = new(); + private readonly LinkedList _list = new(); + + public RollingList(int maximumCount) + { + if (maximumCount <= 0) + throw new ArgumentException(message: null, nameof(maximumCount)); + + MaximumCount = maximumCount; + } + + public int Count => _list.Count; + public int MaximumCount { get; } + + public T this[int index] + { + get + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + return _list.Skip(index).First(); + } + } + + public void Add(T value) + { + lock (_addLock) + { + if (_list.Count == MaximumCount) + { + _list.RemoveFirst(); + } + _list.AddLast(value); + } + } + + public IEnumerator GetEnumerator() => _list.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file diff --git a/MareSynchronos/Utils/ValueProgress.cs b/MareSynchronos/Utils/ValueProgress.cs new file mode 100644 index 0000000..92dfeae --- /dev/null +++ b/MareSynchronos/Utils/ValueProgress.cs @@ -0,0 +1,22 @@ +namespace MareSynchronos.Utils; + +public class ValueProgress : Progress +{ + public T? Value { get; set; } + + protected override void OnReport(T value) + { + base.OnReport(value); + Value = value; + } + + public void Report(T value) + { + OnReport(value); + } + + public void Clear() + { + Value = default; + } +} diff --git a/MareSynchronos/Utils/VariousExtensions.cs b/MareSynchronos/Utils/VariousExtensions.cs new file mode 100644 index 0000000..c916593 --- /dev/null +++ b/MareSynchronos/Utils/VariousExtensions.cs @@ -0,0 +1,230 @@ +using Dalamud.Game.ClientState.Objects.Types; +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.PlayerData.Pairs; +using Microsoft.Extensions.Logging; +using System.Text.Json; + +namespace MareSynchronos.Utils; + +public static class VariousExtensions +{ + public static string ToByteString(this int bytes, bool addSuffix = true) + { + string[] suffix = ["B", "KiB", "MiB", "GiB", "TiB"]; + int i; + double dblSByte = bytes; + for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) + { + dblSByte = bytes / 1024.0; + } + + return addSuffix ? $"{dblSByte:0.00} {suffix[i]}" : $"{dblSByte:0.00}"; + } + + public static string ToByteString(this long bytes, bool addSuffix = true) + { + string[] suffix = ["B", "KiB", "MiB", "GiB", "TiB"]; + int i; + double dblSByte = bytes; + for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) + { + dblSByte = bytes / 1024.0; + } + + return addSuffix ? $"{dblSByte:0.00} {suffix[i]}" : $"{dblSByte:0.00}"; + } + + public static void CancelDispose(this CancellationTokenSource? cts) + { + try + { + cts?.Cancel(); + cts?.Dispose(); + } + catch (ObjectDisposedException) + { + // swallow it + } + } + + public static CancellationTokenSource CancelRecreate(this CancellationTokenSource? cts) + { + cts?.CancelDispose(); + return new CancellationTokenSource(); + } + + public static Dictionary> CheckUpdatedData(this CharacterData newData, Guid applicationBase, + CharacterData? oldData, ILogger logger, PairHandler cachedPlayer, bool forceApplyCustomization, bool forceApplyMods) + { + oldData ??= new(); + var charaDataToUpdate = new Dictionary>(); + foreach (ObjectKind objectKind in Enum.GetValues()) + { + charaDataToUpdate[objectKind] = []; + oldData.FileReplacements.TryGetValue(objectKind, out var existingFileReplacements); + newData.FileReplacements.TryGetValue(objectKind, out var newFileReplacements); + oldData.GlamourerData.TryGetValue(objectKind, out var existingGlamourerData); + newData.GlamourerData.TryGetValue(objectKind, out var newGlamourerData); + + bool hasNewButNotOldFileReplacements = newFileReplacements != null && existingFileReplacements == null; + bool hasOldButNotNewFileReplacements = existingFileReplacements != null && newFileReplacements == null; + + bool hasNewButNotOldGlamourerData = newGlamourerData != null && existingGlamourerData == null; + bool hasOldButNotNewGlamourerData = existingGlamourerData != null && newGlamourerData == null; + + bool hasNewAndOldFileReplacements = newFileReplacements != null && existingFileReplacements != null; + bool hasNewAndOldGlamourerData = newGlamourerData != null && existingGlamourerData != null; + + if (hasNewButNotOldFileReplacements || hasOldButNotNewFileReplacements || hasNewButNotOldGlamourerData || hasOldButNotNewGlamourerData) + { + logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Some new data arrived: NewButNotOldFiles:{hasNewButNotOldFileReplacements}," + + " OldButNotNewFiles:{hasOldButNotNewFileReplacements}, NewButNotOldGlam:{hasNewButNotOldGlamourerData}, OldButNotNewGlam:{hasOldButNotNewGlamourerData}) => {change}, {change2}", + applicationBase, + cachedPlayer, objectKind, hasNewButNotOldFileReplacements, hasOldButNotNewFileReplacements, hasNewButNotOldGlamourerData, hasOldButNotNewGlamourerData, PlayerChanges.ModFiles, PlayerChanges.Glamourer); + charaDataToUpdate[objectKind].Add(PlayerChanges.ModFiles); + charaDataToUpdate[objectKind].Add(PlayerChanges.Glamourer); + charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw); + } + else + { + if (hasNewAndOldFileReplacements) + { + bool listsAreEqual = oldData.FileReplacements[objectKind].SequenceEqual(newData.FileReplacements[objectKind], PlayerData.Data.FileReplacementDataComparer.Instance); + if (!listsAreEqual || forceApplyMods) + { + logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles); + charaDataToUpdate[objectKind].Add(PlayerChanges.ModFiles); + // XXX: This logic is disabled disabled because it seems to skip redrawing for something as basic as toggling a gear mod +#if false + if (forceApplyMods || objectKind != ObjectKind.Player) + { + charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw); + } + else + { + var existingFace = existingFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/face/", StringComparison.OrdinalIgnoreCase))) + .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); + var existingHair = existingFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/hair/", StringComparison.OrdinalIgnoreCase))) + .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); + var existingTail = existingFileReplacements.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 newFace = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/face/", StringComparison.OrdinalIgnoreCase))) + .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); + var newHair = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/hair/", StringComparison.OrdinalIgnoreCase))) + .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"))) + .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"))) + .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, + existingFace.Count, newFace.Count, existingHair.Count, newHair.Count, existingTail.Count, newTail.Count, existingTransients.Count, newTransients.Count); + + var differentFace = !existingFace.SequenceEqual(newFace, PlayerData.Data.FileReplacementDataComparer.Instance); + var differentHair = !existingHair.SequenceEqual(newHair, PlayerData.Data.FileReplacementDataComparer.Instance); + var differentTail = !existingTail.SequenceEqual(newTail, PlayerData.Data.FileReplacementDataComparer.Instance); + var differenTransients = !existingTransients.SequenceEqual(newTransients, PlayerData.Data.FileReplacementDataComparer.Instance); + if (differentFace || differentHair || differentTail || differenTransients) + { + logger.LogDebug("[BASE-{appbase}] Different Subparts: Face: {face}, Hair: {hair}, Tail: {tail}, Transients: {transients} => {change}", applicationBase, + differentFace, differentHair, differentTail, differenTransients, PlayerChanges.ForcedRedraw); + charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw); + } + } +#endif + // XXX: Redraw on mod file changes always + charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw); + } + } + + if (hasNewAndOldGlamourerData) + { + bool glamourerDataDifferent = !string.Equals(oldData.GlamourerData[objectKind], newData.GlamourerData[objectKind], StringComparison.Ordinal); + if (glamourerDataDifferent || forceApplyCustomization) + { + logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Glamourer different) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Glamourer); + charaDataToUpdate[objectKind].Add(PlayerChanges.Glamourer); + } + } + } + + oldData.CustomizePlusData.TryGetValue(objectKind, out var oldCustomizePlusData); + newData.CustomizePlusData.TryGetValue(objectKind, out var newCustomizePlusData); + + oldCustomizePlusData ??= string.Empty; + newCustomizePlusData ??= string.Empty; + + bool customizeDataDifferent = !string.Equals(oldCustomizePlusData, newCustomizePlusData, StringComparison.Ordinal); + if (customizeDataDifferent || (forceApplyCustomization && !string.IsNullOrEmpty(newCustomizePlusData))) + { + logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff customize data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Customize); + charaDataToUpdate[objectKind].Add(PlayerChanges.Customize); + } + + if (objectKind != ObjectKind.Player) continue; + + bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal); + if (manipDataDifferent || forceApplyMods) + { + logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip); + charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip); + charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw); + } + + bool heelsOffsetDifferent = !string.Equals(oldData.HeelsData, newData.HeelsData, StringComparison.Ordinal); + if (heelsOffsetDifferent || (forceApplyCustomization && !string.IsNullOrEmpty(newData.HeelsData))) + { + logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff heels data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Heels); + charaDataToUpdate[objectKind].Add(PlayerChanges.Heels); + } + + bool honorificDataDifferent = !string.Equals(oldData.HonorificData, newData.HonorificData, StringComparison.Ordinal); + if (honorificDataDifferent || (forceApplyCustomization && !string.IsNullOrEmpty(newData.HonorificData))) + { + logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff honorific data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Honorific); + charaDataToUpdate[objectKind].Add(PlayerChanges.Honorific); + } + + bool petNamesDataDifferent = !string.Equals(oldData.PetNamesData, newData.PetNamesData, StringComparison.Ordinal); + if (petNamesDataDifferent || (forceApplyCustomization && !string.IsNullOrEmpty(newData.PetNamesData))) + { + logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff petnames data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.PetNames); + charaDataToUpdate[objectKind].Add(PlayerChanges.PetNames); + } + + bool moodlesDataDifferent = !string.Equals(oldData.MoodlesData, newData.MoodlesData, StringComparison.Ordinal); + if (moodlesDataDifferent || (forceApplyCustomization && !string.IsNullOrEmpty(newData.MoodlesData))) + { + logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff moodles data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Moodles); + charaDataToUpdate[objectKind].Add(PlayerChanges.Moodles); + } + } + + foreach (KeyValuePair> data in charaDataToUpdate.ToList()) + { + if (!data.Value.Any()) charaDataToUpdate.Remove(data.Key); + else charaDataToUpdate[data.Key] = [.. data.Value.OrderByDescending(p => (int)p)]; + } + + return charaDataToUpdate; + } + + public static T DeepClone(this T obj) + { + return JsonSerializer.Deserialize(JsonSerializer.Serialize(obj))!; + } + + public static unsafe int? ObjectTableIndex(this IGameObject? gameObject) + { + if (gameObject == null || gameObject.Address == IntPtr.Zero) + { + return null; + } + + return ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address)->ObjectIndex; + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/AccountRegistrationService.cs b/MareSynchronos/WebAPI/AccountRegistrationService.cs new file mode 100644 index 0000000..4b5d81b --- /dev/null +++ b/MareSynchronos/WebAPI/AccountRegistrationService.cs @@ -0,0 +1,70 @@ +using MareSynchronos.API.Dto.Account; +using MareSynchronos.API.Routes; +using MareSynchronos.Services; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI.SignalR; +using Microsoft.Extensions.Logging; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Reflection; +using System.Security.Cryptography; + +namespace MareSynchronos.WebAPI; + +public sealed class AccountRegistrationService : IDisposable +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly ServerConfigurationManager _serverManager; + + private string GenerateSecretKey() + { + return Convert.ToHexString(SHA256.HashData(RandomNumberGenerator.GetBytes(64))); + } + + public AccountRegistrationService(ILogger logger, ServerConfigurationManager serverManager) + { + _logger = logger; + _serverManager = serverManager; + _httpClient = new( + new HttpClientHandler + { + AllowAutoRedirect = true, + MaxAutomaticRedirections = 5 + } + ); + var ver = Assembly.GetExecutingAssembly().GetName().Version; + _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); + } + + public void Dispose() + { + _httpClient.Dispose(); + } + + public async Task RegisterAccount(CancellationToken token) + { + var secretKey = GenerateSecretKey(); + var hashedSecretKey = secretKey.GetHash256(); + + Uri postUri = MareAuth.AuthRegisterV2FullPath(new Uri(_serverManager.CurrentApiUrl + .Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase) + .Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase))); + + var result = await _httpClient.PostAsync(postUri, new FormUrlEncodedContent([ + new("hashedSecretKey", hashedSecretKey) + ]), token).ConfigureAwait(false); + result.EnsureSuccessStatusCode(); + + var response = await result.Content.ReadFromJsonAsync(token).ConfigureAwait(false) ?? new(); + + return new RegisterReplyDto() + { + Success = response.Success, + ErrorMessage = response.ErrorMessage, + UID = response.UID, + SecretKey = secretKey + }; + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/FileDownloadManager.cs b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs new file mode 100644 index 0000000..69439fa --- /dev/null +++ b/MareSynchronos/WebAPI/Files/FileDownloadManager.cs @@ -0,0 +1,510 @@ +using Dalamud.Utility; +using K4os.Compression.LZ4.Streams; +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.Files; +using MareSynchronos.API.Routes; +using MareSynchronos.FileCache; +using MareSynchronos.PlayerData.Handlers; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI.Files.Models; +using Microsoft.Extensions.Logging; +using System.Net; +using System.Net.Http.Json; +using System.Security.Cryptography; + +namespace MareSynchronos.WebAPI.Files; + +public partial class FileDownloadManager : DisposableMediatorSubscriberBase +{ + private readonly Dictionary _downloadStatus; + private readonly FileCompactor _fileCompactor; + private readonly FileCacheManager _fileDbManager; + private readonly FileTransferOrchestrator _orchestrator; + private readonly List _activeDownloadStreams; + + public FileDownloadManager(ILogger logger, MareMediator mediator, + FileTransferOrchestrator orchestrator, + FileCacheManager fileCacheManager, FileCompactor fileCompactor) : base(logger, mediator) + { + _downloadStatus = new Dictionary(StringComparer.Ordinal); + _orchestrator = orchestrator; + _fileDbManager = fileCacheManager; + _fileCompactor = fileCompactor; + _activeDownloadStreams = []; + + Mediator.Subscribe(this, (msg) => + { + if (!_activeDownloadStreams.Any()) return; + var newLimit = _orchestrator.DownloadLimitPerSlot(); + Logger.LogTrace("Setting new Download Speed Limit to {newLimit}", newLimit); + foreach (var stream in _activeDownloadStreams) + { + stream.BandwidthLimit = newLimit; + } + }); + } + + public List CurrentDownloads { get; private set; } = []; + + public List ForbiddenTransfers => _orchestrator.ForbiddenTransfers; + + public bool IsDownloading => !CurrentDownloads.Any(); + + public void ClearDownload() + { + CurrentDownloads.Clear(); + _downloadStatus.Clear(); + } + + public async Task DownloadFiles(GameObjectHandler gameObject, List fileReplacementDto, CancellationToken ct) + { + Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles))); + try + { + await DownloadFilesInternal(gameObject, fileReplacementDto, ct).ConfigureAwait(false); + } + catch + { + ClearDownload(); + } + finally + { + Mediator.Publish(new DownloadFinishedMessage(gameObject)); + Mediator.Publish(new ResumeScanMessage(nameof(DownloadFiles))); + } + } + + protected override void Dispose(bool disposing) + { + ClearDownload(); + foreach (var stream in _activeDownloadStreams.ToList()) + { + try + { + stream.Dispose(); + } + catch + { + // do nothing + // + } + } + base.Dispose(disposing); + } + + private static byte ConvertReadByte(int byteOrEof) + { + if (byteOrEof == -1) + { + throw new EndOfStreamException(); + } + + return (byte)byteOrEof; + } + + private static (string fileHash, long fileLengthBytes) ReadBlockFileHeader(FileStream fileBlockStream) + { + List hashName = []; + List fileLength = []; + var separator = (char)ConvertReadByte(fileBlockStream.ReadByte()); + if (separator != '#') throw new InvalidDataException("Data is invalid, first char is not #"); + + bool readHash = false; + while (true) + { + int readByte = fileBlockStream.ReadByte(); + if (readByte == -1) + throw new EndOfStreamException(); + + var readChar = (char)ConvertReadByte(readByte); + if (readChar == ':') + { + readHash = true; + continue; + } + if (readChar == '#') break; + if (!readHash) hashName.Add(readChar); + else fileLength.Add(readChar); + } + if (fileLength.Count == 0) + fileLength.Add('0'); + return (string.Join("", hashName), long.Parse(string.Join("", fileLength))); + } + + private async Task DownloadAndMungeFileHttpClient(string downloadGroup, Guid requestId, List fileTransfer, string tempPath, IProgress progress, CancellationToken ct) + { + Logger.LogDebug("GUID {requestId} on server {uri} for files {files}", requestId, fileTransfer[0].DownloadUri, string.Join(", ", fileTransfer.Select(c => c.Hash).ToList())); + + await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false); + + _downloadStatus[downloadGroup].DownloadStatus = DownloadStatus.Downloading; + + HttpResponseMessage response = null!; + var requestUrl = MareFiles.CacheGetFullPath(fileTransfer[0].DownloadUri, requestId); + + Logger.LogDebug("Downloading {requestUrl} for request {id}", requestUrl, requestId); + try + { + response = await _orchestrator.SendRequestAsync(HttpMethod.Get, requestUrl, ct, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + catch (HttpRequestException ex) + { + Logger.LogWarning(ex, "Error during download of {requestUrl}, HttpStatusCode: {code}", requestUrl, ex.StatusCode); + if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized) + { + throw new InvalidDataException($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex); + } + } + + ThrottledStream? stream = null; + try + { + var fileStream = File.Create(tempPath); + await using (fileStream.ConfigureAwait(false)) + { + var bufferSize = response.Content.Headers.ContentLength > 1024 * 1024 ? 65536 : 8196; + var buffer = new byte[bufferSize]; + + var bytesRead = 0; + var limit = _orchestrator.DownloadLimitPerSlot(); + Logger.LogTrace("Starting Download of {id} with a speed limit of {limit} to {tempPath}", requestId, limit, tempPath); + stream = new ThrottledStream(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit); + _activeDownloadStreams.Add(stream); + while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0) + { + ct.ThrowIfCancellationRequested(); + + await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct).ConfigureAwait(false); + + progress.Report(bytesRead); + } + + Logger.LogDebug("{requestUrl} downloaded to {tempPath}", requestUrl, tempPath); + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception) + { + try + { + if (!tempPath.IsNullOrEmpty()) + File.Delete(tempPath); + } + catch + { + // ignore if file deletion fails + } + throw; + } + finally + { + if (stream != null) + { + _activeDownloadStreams.Remove(stream); + await stream.DisposeAsync().ConfigureAwait(false); + } + } + } + + public async Task> InitiateDownloadList(GameObjectHandler gameObjectHandler, List fileReplacement, CancellationToken ct) + { + Logger.LogDebug("Download start: {id}", gameObjectHandler.Name); + + List downloadFileInfoFromService = + [ + .. await FilesGetSizes(fileReplacement.Select(f => f.Hash).Distinct(StringComparer.Ordinal).ToList(), ct).ConfigureAwait(false), + ]; + + Logger.LogDebug("Files with size 0 or less: {files}", string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash))); + + foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden)) + { + if (!_orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal))) + { + _orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto)); + } + } + + CurrentDownloads = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d)) + .Where(d => d.CanBeTransferred).ToList(); + + return CurrentDownloads; + } + + private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List fileReplacement, CancellationToken ct) + { + var downloadGroups = CurrentDownloads.GroupBy(f => f.DownloadUri.Host + ":" + f.DownloadUri.Port, StringComparer.Ordinal); + + foreach (var downloadGroup in downloadGroups) + { + _downloadStatus[downloadGroup.Key] = new FileDownloadStatus() + { + DownloadStatus = DownloadStatus.Initializing, + TotalBytes = downloadGroup.Sum(c => c.Total), + TotalFiles = 1, + TransferredBytes = 0, + TransferredFiles = 0 + }; + } + + Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus)); + + await Parallel.ForEachAsync(downloadGroups, new ParallelOptions() + { + MaxDegreeOfParallelism = downloadGroups.Count(), + CancellationToken = ct, + }, + async (fileGroup, token) => + { + // let server predownload files + var requestIdResponse = await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri), + fileGroup.Select(c => c.Hash), token).ConfigureAwait(false); + Logger.LogDebug("Sent request for {n} files on server {uri} with result {result}", fileGroup.Count(), fileGroup.First().DownloadUri, + await requestIdResponse.Content.ReadAsStringAsync(token).ConfigureAwait(false)); + + Guid requestId = Guid.Parse((await requestIdResponse.Content.ReadAsStringAsync().ConfigureAwait(false)).Trim('"')); + + Logger.LogDebug("GUID {requestId} for {n} files on server {uri}", requestId, fileGroup.Count(), fileGroup.First().DownloadUri); + + var blockFile = _fileDbManager.GetCacheFilePath(requestId.ToString("N"), "blk"); + FileInfo fi = new(blockFile); + try + { + _downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForSlot; + await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false); + _downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForQueue; + Progress progress = new((bytesDownloaded) => + { + try + { + if (!_downloadStatus.TryGetValue(fileGroup.Key, out FileDownloadStatus? value)) return; + value.TransferredBytes += bytesDownloaded; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Could not set download progress"); + } + }); + await DownloadAndMungeFileHttpClient(fileGroup.Key, requestId, [.. fileGroup], blockFile, progress, token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Logger.LogDebug("{dlName}: Detected cancellation of download, partially extracting files for {id}", fi.Name, gameObjectHandler); + } + catch (Exception ex) + { + _orchestrator.ReleaseDownloadSlot(); + File.Delete(blockFile); + Logger.LogError(ex, "{dlName}: Error during download of {id}", fi.Name, requestId); + ClearDownload(); + return; + } + + FileStream? fileBlockStream = null; + var threadCount = Math.Clamp((int)(Environment.ProcessorCount / 2.0f), 2, 8); + var tasks = new List(); + try + { + if (_downloadStatus.TryGetValue(fileGroup.Key, out var status)) + { + status.TransferredFiles = 1; + status.DownloadStatus = DownloadStatus.Decompressing; + } + fileBlockStream = File.OpenRead(blockFile); + while (fileBlockStream.Position < fileBlockStream.Length) + { + (string fileHash, long fileLengthBytes) = ReadBlockFileHeader(fileBlockStream); + var chunkPosition = fileBlockStream.Position; + fileBlockStream.Position += fileLengthBytes; + + while (tasks.Count > threadCount && tasks.Where(t => !t.IsCompleted).Count() > 4) + await Task.Delay(10, CancellationToken.None).ConfigureAwait(false); + + var fileExtension = fileReplacement.First(f => string.Equals(f.Hash, fileHash, StringComparison.OrdinalIgnoreCase)).GamePaths[0].Split(".")[^1]; + var tmpPath = _fileDbManager.GetCacheFilePath(Guid.NewGuid().ToString(), "tmp"); + var filePath = _fileDbManager.GetCacheFilePath(fileHash, fileExtension); + + Logger.LogDebug("{dlName}: Decompressing {file}:{le} => {dest}", fi.Name, fileHash, fileLengthBytes, filePath); + + tasks.Add(Task.Run(() => { + try + { + using var tmpFileStream = new HashingStream(new FileStream(tmpPath, new FileStreamOptions() + { + Mode = FileMode.CreateNew, + Access = FileAccess.Write, + Share = FileShare.None + }), SHA1.Create()); + + using var fileChunkStream = new FileStream(blockFile, new FileStreamOptions() + { + BufferSize = 80000, + Mode = FileMode.Open, + Access = FileAccess.Read + }); + fileChunkStream.Position = chunkPosition; + + using var innerFileStream = new LimitedStream(fileChunkStream, fileLengthBytes); + using var decoder = LZ4Frame.Decode(innerFileStream); + long startPos = fileChunkStream.Position; + decoder.AsStream().CopyTo(tmpFileStream); + long readBytes = fileChunkStream.Position - startPos; + + if (readBytes != fileLengthBytes) + { + throw new EndOfStreamException(); + } + + string calculatedHash = BitConverter.ToString(tmpFileStream.Finish()).Replace("-", "", StringComparison.Ordinal); + + if (!calculatedHash.Equals(fileHash, StringComparison.Ordinal)) + { + Logger.LogError("Hash mismatch after extracting, got {hash}, expected {expectedHash}, deleting file", calculatedHash, fileHash); + return; + } + + tmpFileStream.Close(); + _fileCompactor.RenameAndCompact(filePath, tmpPath); + PersistFileToStorage(fileHash, filePath, fileLengthBytes); + } + catch (EndOfStreamException) + { + Logger.LogWarning("{dlName}: Failure to extract file {fileHash}, stream ended prematurely", fi.Name, fileHash); + } + catch (Exception e) + { + Logger.LogWarning(e, "{dlName}: Error during decompression of {hash}", fi.Name, fileHash); + + foreach (var fr in fileReplacement) + Logger.LogWarning(" - {h}: {x}", fr.Hash, fr.GamePaths[0]); + } + finally + { + if (File.Exists(tmpPath)) + File.Delete(tmpPath); + } + }, CancellationToken.None)); + } + + Task.WaitAll([..tasks], CancellationToken.None); + } + catch (EndOfStreamException) + { + Logger.LogDebug("{dlName}: Failure to extract file header data, stream ended", fi.Name); + } + catch (Exception ex) + { + Logger.LogError(ex, "{dlName}: Error during block file read", fi.Name); + } + finally + { + Task.WaitAll([..tasks], CancellationToken.None); + _orchestrator.ReleaseDownloadSlot(); + if (fileBlockStream != null) + await fileBlockStream.DisposeAsync().ConfigureAwait(false); + File.Delete(blockFile); + } + }).ConfigureAwait(false); + + Logger.LogDebug("Download end: {id}", gameObjectHandler); + + ClearDownload(); + } + + private async Task> FilesGetSizes(List hashes, CancellationToken ct) + { + if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized"); + var response = await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.ServerFilesGetSizesFullPath(_orchestrator.FilesCdnUri!), hashes, ct).ConfigureAwait(false); + return await response.Content.ReadFromJsonAsync>(cancellationToken: ct).ConfigureAwait(false) ?? []; + } + + private void PersistFileToStorage(string fileHash, string filePath, long? compressedSize = null) + { + try + { + var entry = _fileDbManager.CreateCacheEntry(filePath, fileHash); + if (entry != null && !string.Equals(entry.Hash, fileHash, StringComparison.OrdinalIgnoreCase)) + { + _fileDbManager.RemoveHashedFile(entry.Hash, entry.PrefixedFilePath); + entry = null; + } + if (entry != null) + entry.CompressedSize = compressedSize; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error creating cache entry"); + } + } + + private async Task WaitForDownloadReady(List downloadFileTransfer, Guid requestId, CancellationToken downloadCt) + { + bool alreadyCancelled = false; + try + { + CancellationTokenSource localTimeoutCts = new(); + localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5)); + CancellationTokenSource composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token); + + while (!_orchestrator.IsDownloadReady(requestId)) + { + try + { + await Task.Delay(250, composite.Token).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + if (downloadCt.IsCancellationRequested) throw; + + var req = await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestCheckQueueFullPath(downloadFileTransfer[0].DownloadUri, requestId), + downloadFileTransfer.Select(c => c.Hash).ToList(), downloadCt).ConfigureAwait(false); + req.EnsureSuccessStatusCode(); + localTimeoutCts.Dispose(); + composite.Dispose(); + localTimeoutCts = new(); + localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5)); + composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token); + } + } + + localTimeoutCts.Dispose(); + composite.Dispose(); + + Logger.LogDebug("Download {requestId} ready", requestId); + } + catch (TaskCanceledException) + { + try + { + await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer[0].DownloadUri, requestId)).ConfigureAwait(false); + alreadyCancelled = true; + } + catch + { + // ignore whatever happens here + } + + throw; + } + finally + { + if (downloadCt.IsCancellationRequested && !alreadyCancelled) + { + try + { + await _orchestrator.SendRequestAsync(HttpMethod.Get, MareFiles.RequestCancelFullPath(downloadFileTransfer[0].DownloadUri, requestId)).ConfigureAwait(false); + } + catch + { + // ignore whatever happens here + } + } + _orchestrator.ClearDownloadRequest(requestId); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs b/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs new file mode 100644 index 0000000..1735f72 --- /dev/null +++ b/MareSynchronos/WebAPI/Files/FileTransferOrchestrator.cs @@ -0,0 +1,177 @@ +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services.Mediator; +using MareSynchronos.WebAPI.Files.Models; +using MareSynchronos.WebAPI.SignalR; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Reflection; + +namespace MareSynchronos.WebAPI.Files; + +public class FileTransferOrchestrator : DisposableMediatorSubscriberBase +{ + private readonly ConcurrentDictionary _downloadReady = new(); + private readonly HttpClient _httpClient; + private readonly MareConfigService _mareConfig; + private readonly Lock _semaphoreModificationLock = new(); + private readonly TokenProvider _tokenProvider; + private int _availableDownloadSlots; + private SemaphoreSlim _downloadSemaphore; + private int CurrentlyUsedDownloadSlots => _availableDownloadSlots - _downloadSemaphore.CurrentCount; + + public FileTransferOrchestrator(ILogger logger, MareConfigService mareConfig, + MareMediator mediator, TokenProvider tokenProvider) : base(logger, mediator) + { + _mareConfig = mareConfig; + _tokenProvider = tokenProvider; + _httpClient = new() + { + Timeout = TimeSpan.FromSeconds(3000) + }; + var ver = Assembly.GetExecutingAssembly().GetName().Version; + _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); + + _availableDownloadSlots = mareConfig.Current.ParallelDownloads; + _downloadSemaphore = new(_availableDownloadSlots, _availableDownloadSlots); + + Mediator.Subscribe(this, (msg) => + { + FilesCdnUri = msg.Connection.ServerInfo.FileServerAddress; + }); + + Mediator.Subscribe(this, (msg) => + { + FilesCdnUri = null; + }); + Mediator.Subscribe(this, (msg) => + { + _downloadReady[msg.RequestId] = true; + }); + } + + public Uri? FilesCdnUri { private set; get; } + public List ForbiddenTransfers { get; } = []; + public bool IsInitialized => FilesCdnUri != null; + + public void ClearDownloadRequest(Guid guid) + { + _downloadReady.Remove(guid, out _); + } + + public bool IsDownloadReady(Guid guid) + { + if (_downloadReady.TryGetValue(guid, out bool isReady) && isReady) + { + return true; + } + + return false; + } + + public void ReleaseDownloadSlot() + { + try + { + _downloadSemaphore.Release(); + Mediator.Publish(new DownloadLimitChangedMessage()); + } + catch (SemaphoreFullException) + { + // ignore + } + } + + public async Task SendRequestAsync(HttpMethod method, Uri uri, + CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) + { + using var requestMessage = new HttpRequestMessage(method, uri); + return await SendRequestInternalAsync(requestMessage, ct, httpCompletionOption).ConfigureAwait(false); + } + + public async Task SendRequestAsync(HttpMethod method, Uri uri, T content, CancellationToken ct) 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).ConfigureAwait(false); + } + + public async Task SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content, CancellationToken ct) + { + using var requestMessage = new HttpRequestMessage(method, uri); + requestMessage.Content = content; + return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false); + } + + public async Task WaitForDownloadSlotAsync(CancellationToken token) + { + lock (_semaphoreModificationLock) + { + if (_availableDownloadSlots != _mareConfig.Current.ParallelDownloads && _availableDownloadSlots == _downloadSemaphore.CurrentCount) + { + _availableDownloadSlots = _mareConfig.Current.ParallelDownloads; + _downloadSemaphore = new(_availableDownloadSlots, _availableDownloadSlots); + } + } + + await _downloadSemaphore.WaitAsync(token).ConfigureAwait(false); + Mediator.Publish(new DownloadLimitChangedMessage()); + } + + public long DownloadLimitPerSlot() + { + var limit = _mareConfig.Current.DownloadSpeedLimitInBytes; + if (limit <= 0) return 0; + limit = _mareConfig.Current.DownloadSpeedType switch + { + MareConfiguration.Models.DownloadSpeeds.Bps => limit, + MareConfiguration.Models.DownloadSpeeds.KBps => limit * 1024, + MareConfiguration.Models.DownloadSpeeds.MBps => limit * 1024 * 1024, + _ => limit, + }; + var currentUsedDlSlots = CurrentlyUsedDownloadSlots; + var avaialble = _availableDownloadSlots; + var currentCount = _downloadSemaphore.CurrentCount; + var dividedLimit = limit / (currentUsedDlSlots == 0 ? 1 : currentUsedDlSlots); + if (dividedLimit < 0) + { + Logger.LogWarning("Calculated Bandwidth Limit is negative, returning Infinity: {value}, CurrentlyUsedDownloadSlots is {currentSlots}, " + + "DownloadSpeedLimit is {limit}, available slots: {avail}, current count: {count}", dividedLimit, currentUsedDlSlots, limit, avaialble, currentCount); + return long.MaxValue; + } + return Math.Clamp(dividedLimit, 1, long.MaxValue); + } + + private async Task SendRequestInternalAsync(HttpRequestMessage requestMessage, + CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead) + { + 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 (Exception ex) + { + Logger.LogWarning(ex, "Error during SendRequestInternal for {uri}", requestMessage.RequestUri); + throw; + } + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/FileUploadManager.cs b/MareSynchronos/WebAPI/Files/FileUploadManager.cs new file mode 100644 index 0000000..10b7f1e --- /dev/null +++ b/MareSynchronos/WebAPI/Files/FileUploadManager.cs @@ -0,0 +1,289 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.Files; +using MareSynchronos.API.Routes; +using MareSynchronos.FileCache; +using MareSynchronos.MareConfiguration; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.UI; +using MareSynchronos.WebAPI.Files.Models; +using Microsoft.Extensions.Logging; +using System.Net.Http.Headers; +using System.Net.Http.Json; + + +namespace MareSynchronos.WebAPI.Files; + +public sealed class FileUploadManager : DisposableMediatorSubscriberBase +{ + private readonly FileCacheManager _fileDbManager; + private readonly MareConfigService _mareConfigService; + private readonly FileTransferOrchestrator _orchestrator; + private readonly ServerConfigurationManager _serverManager; + private readonly Dictionary _verifiedUploadedHashes = new(StringComparer.Ordinal); + private CancellationTokenSource? _uploadCancellationTokenSource = new(); + + public FileUploadManager(ILogger logger, MareMediator mediator, + MareConfigService mareConfigService, + FileTransferOrchestrator orchestrator, + FileCacheManager fileDbManager, + ServerConfigurationManager serverManager) : base(logger, mediator) + { + _mareConfigService = mareConfigService; + _orchestrator = orchestrator; + _fileDbManager = fileDbManager; + _serverManager = serverManager; + + Mediator.Subscribe(this, (msg) => + { + Reset(); + }); + } + + public List CurrentUploads { get; } = []; + public bool IsUploading => CurrentUploads.Count > 0; + + public bool CancelUpload() + { + if (CurrentUploads.Any()) + { + Logger.LogDebug("Cancelling current upload"); + _uploadCancellationTokenSource?.Cancel(); + _uploadCancellationTokenSource?.Dispose(); + _uploadCancellationTokenSource = null; + CurrentUploads.Clear(); + return true; + } + + return false; + } + + public async Task DeleteAllFiles() + { + if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized"); + + await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.ServerFilesDeleteAllFullPath(_orchestrator.FilesCdnUri!)).ConfigureAwait(false); + } + + public async Task> UploadFiles(List hashesToUpload, IProgress progress, CancellationToken? ct = null) + { + Logger.LogDebug("Trying to upload files"); + var filesPresentLocally = hashesToUpload.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal); + var locallyMissingFiles = hashesToUpload.Except(filesPresentLocally, StringComparer.Ordinal).ToList(); + if (locallyMissingFiles.Any()) + { + return locallyMissingFiles; + } + + progress.Report($"Starting upload for {filesPresentLocally.Count} files"); + + var filesToUpload = await FilesSend([.. filesPresentLocally], [], ct ?? CancellationToken.None).ConfigureAwait(false); + + if (filesToUpload.Exists(f => f.IsForbidden)) + { + return [.. filesToUpload.Where(f => f.IsForbidden).Select(f => f.Hash)]; + } + + Task uploadTask = Task.CompletedTask; + int i = 1; + foreach (var file in filesToUpload) + { + progress.Report($"Uploading file {i++}/{filesToUpload.Count}. Please wait until the upload is completed."); + Logger.LogDebug("[{hash}] Compressing", file); + var data = await _fileDbManager.GetCompressedFileData(file.Hash, ct ?? CancellationToken.None).ConfigureAwait(false); + Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, _fileDbManager.GetFileCacheByHash(data.Item1)!.ResolvedFilepath); + await uploadTask.ConfigureAwait(false); + uploadTask = UploadFile(data.Item2, file.Hash, false, ct ?? CancellationToken.None); + (ct ?? CancellationToken.None).ThrowIfCancellationRequested(); + } + + await uploadTask.ConfigureAwait(false); + + return []; + } + + public async Task UploadFiles(CharacterData data, List visiblePlayers) + { + CancelUpload(); + + _uploadCancellationTokenSource = new CancellationTokenSource(); + var uploadToken = _uploadCancellationTokenSource.Token; + Logger.LogDebug("Sending Character data {hash} to service {url}", data.DataHash.Value, _serverManager.CurrentRealApiUrl); + + HashSet unverifiedUploads = GetUnverifiedFiles(data); + if (unverifiedUploads.Any()) + { + await UploadUnverifiedFiles(unverifiedUploads, visiblePlayers, uploadToken).ConfigureAwait(false); + Logger.LogInformation("Upload complete for {hash}", data.DataHash.Value); + } + + foreach (var kvp in data.FileReplacements) + { + data.FileReplacements[kvp.Key].RemoveAll(i => _orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, i.Hash, StringComparison.OrdinalIgnoreCase))); + } + + return data; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + Reset(); + } + + private async Task> FilesSend(List hashes, List uids, CancellationToken ct) + { + if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized"); + FilesSendDto filesSendDto = new() + { + FileHashes = hashes, + UIDs = uids + }; + var response = await _orchestrator.SendRequestAsync(HttpMethod.Post, MareFiles.ServerFilesFilesSendFullPath(_orchestrator.FilesCdnUri!), filesSendDto, ct).ConfigureAwait(false); + return await response.Content.ReadFromJsonAsync>(cancellationToken: ct).ConfigureAwait(false) ?? []; + } + + private HashSet GetUnverifiedFiles(CharacterData data) + { + HashSet unverifiedUploadHashes = new(StringComparer.Ordinal); + foreach (var item in data.FileReplacements.SelectMany(c => c.Value.Where(f => string.IsNullOrEmpty(f.FileSwapPath)).Select(v => v.Hash).Distinct(StringComparer.Ordinal)).Distinct(StringComparer.Ordinal).ToList()) + { + if (!_verifiedUploadedHashes.TryGetValue(item, out var verifiedTime)) + { + verifiedTime = DateTime.MinValue; + } + + if (verifiedTime < DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(10))) + { + Logger.LogTrace("Verifying {item}, last verified: {date}", item, verifiedTime); + unverifiedUploadHashes.Add(item); + } + } + + return unverifiedUploadHashes; + } + + private void Reset() + { + _uploadCancellationTokenSource?.Cancel(); + _uploadCancellationTokenSource?.Dispose(); + _uploadCancellationTokenSource = null; + CurrentUploads.Clear(); + _verifiedUploadedHashes.Clear(); + } + + private async Task UploadFile(byte[] compressedFile, string fileHash, bool postProgress, CancellationToken uploadToken) + { + if (!_orchestrator.IsInitialized) throw new InvalidOperationException("FileTransferManager is not initialized"); + + Logger.LogInformation("[{hash}] Uploading {size}", fileHash, UiSharedService.ByteToString(compressedFile.Length)); + + if (uploadToken.IsCancellationRequested) return; + + try + { + await UploadFileStream(compressedFile, fileHash, munged: false, postProgress, uploadToken).ConfigureAwait(false); + _verifiedUploadedHashes[fileHash] = DateTime.UtcNow; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "[{hash}] File upload cancelled", fileHash); + } + } + + private async Task UploadFileStream(byte[] compressedFile, string fileHash, bool munged, bool postProgress, CancellationToken uploadToken) + { + if (munged) + throw new InvalidOperationException(); + + using var ms = new MemoryStream(compressedFile); + + Progress? prog = !postProgress ? null : new((prog) => + { + try + { + CurrentUploads.Single(f => string.Equals(f.Hash, fileHash, StringComparison.Ordinal)).Transferred = prog.Uploaded; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "[{hash}] Could not set upload progress", fileHash); + } + }); + + var streamContent = new ProgressableStreamContent(ms, prog); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + HttpResponseMessage response; + if (!munged) + response = await _orchestrator.SendRequestStreamAsync(HttpMethod.Post, MareFiles.ServerFilesUploadFullPath(_orchestrator.FilesCdnUri!, fileHash), streamContent, uploadToken).ConfigureAwait(false); + else + response = await _orchestrator.SendRequestStreamAsync(HttpMethod.Post, MareFiles.ServerFilesUploadMunged(_orchestrator.FilesCdnUri!, fileHash), streamContent, uploadToken).ConfigureAwait(false); + Logger.LogDebug("[{hash}] Upload Status: {status}", fileHash, response.StatusCode); + } + + private async Task UploadUnverifiedFiles(HashSet unverifiedUploadHashes, List visiblePlayers, CancellationToken uploadToken) + { + unverifiedUploadHashes = unverifiedUploadHashes.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal); + + Logger.LogDebug("Verifying {count} files", unverifiedUploadHashes.Count); + var filesToUpload = await FilesSend([.. unverifiedUploadHashes], visiblePlayers.Select(p => p.UID).ToList(), uploadToken).ConfigureAwait(false); + + foreach (var file in filesToUpload.Where(f => !f.IsForbidden).DistinctBy(f => f.Hash)) + { + try + { + CurrentUploads.Add(new UploadFileTransfer(file) + { + Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length, + }); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Tried to request file {hash} but file was not present", file.Hash); + } + } + + foreach (var file in filesToUpload.Where(c => c.IsForbidden)) + { + if (_orchestrator.ForbiddenTransfers.TrueForAll(f => !string.Equals(f.Hash, file.Hash, StringComparison.Ordinal))) + { + _orchestrator.ForbiddenTransfers.Add(new UploadFileTransfer(file) + { + LocalFile = _fileDbManager.GetFileCacheByHash(file.Hash)?.ResolvedFilepath ?? string.Empty, + }); + } + + _verifiedUploadedHashes[file.Hash] = DateTime.UtcNow; + } + + var totalSize = CurrentUploads.Sum(c => c.Total); + Logger.LogDebug("Compressing and uploading files"); + Task uploadTask = Task.CompletedTask; + foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList()) + { + Logger.LogDebug("[{hash}] Compressing", file); + var data = await _fileDbManager.GetCompressedFileData(file.Hash, uploadToken).ConfigureAwait(false); + CurrentUploads.Single(e => string.Equals(e.Hash, file.Hash, StringComparison.Ordinal)).Total = data.Item2.Length; + Logger.LogDebug("[{hash}] Starting upload for {filePath}", file.Hash, _fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath); + await uploadTask.ConfigureAwait(false); + uploadTask = UploadFile(data.Item2, file.Hash, true, uploadToken); + uploadToken.ThrowIfCancellationRequested(); + } + + if (CurrentUploads.Any()) + { + await uploadTask.ConfigureAwait(false); + + var compressedSize = CurrentUploads.Sum(c => c.Total); + Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize)); + + _fileDbManager.WriteOutFullCsv(); + } + + foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Exists(u => string.Equals(u.Hash, c, StringComparison.Ordinal)))) + { + _verifiedUploadedHashes[file] = DateTime.UtcNow; + } + + CurrentUploads.Clear(); + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs b/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs new file mode 100644 index 0000000..92f357a --- /dev/null +++ b/MareSynchronos/WebAPI/Files/Models/DownloadFileTransfer.cs @@ -0,0 +1,24 @@ +using MareSynchronos.API.Dto.Files; + +namespace MareSynchronos.WebAPI.Files.Models; + +public class DownloadFileTransfer : FileTransfer +{ + public DownloadFileTransfer(DownloadFileDto dto) : base(dto) + { + } + + public override bool CanBeTransferred => Dto.FileExists && !Dto.IsForbidden && Dto.Size > 0; + public Uri DownloadUri => new(Dto.Url); + public override long Total + { + set + { + // nothing to set + } + get => Dto.Size; + } + + public long TotalRaw => 0; // XXX + private DownloadFileDto Dto => (DownloadFileDto)TransferDto; +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs b/MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs new file mode 100644 index 0000000..13202c8 --- /dev/null +++ b/MareSynchronos/WebAPI/Files/Models/DownloadStatus.cs @@ -0,0 +1,10 @@ +namespace MareSynchronos.WebAPI.Files.Models; + +public enum DownloadStatus +{ + Initializing, + WaitingForSlot, + WaitingForQueue, + Downloading, + Decompressing +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs b/MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs new file mode 100644 index 0000000..8a386ce --- /dev/null +++ b/MareSynchronos/WebAPI/Files/Models/FileDownloadStatus.cs @@ -0,0 +1,10 @@ +namespace MareSynchronos.WebAPI.Files.Models; + +public class FileDownloadStatus +{ + public DownloadStatus DownloadStatus { get; set; } + public long TotalBytes { get; set; } + public int TotalFiles { get; set; } + public long TransferredBytes { get; set; } + public int TransferredFiles { get; set; } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/Models/FileTransfer.cs b/MareSynchronos/WebAPI/Files/Models/FileTransfer.cs new file mode 100644 index 0000000..f3c0f1e --- /dev/null +++ b/MareSynchronos/WebAPI/Files/Models/FileTransfer.cs @@ -0,0 +1,27 @@ +using MareSynchronos.API.Dto.Files; + +namespace MareSynchronos.WebAPI.Files.Models; + +public abstract class FileTransfer +{ + protected readonly ITransferFileDto TransferDto; + + protected FileTransfer(ITransferFileDto transferDto) + { + TransferDto = transferDto; + } + + public virtual bool CanBeTransferred => !TransferDto.IsForbidden && (TransferDto is not DownloadFileDto dto || dto.FileExists); + public string ForbiddenBy => TransferDto.ForbiddenBy; + public string Hash => TransferDto.Hash; + public bool IsForbidden => TransferDto.IsForbidden; + public bool IsInTransfer => Transferred != Total && Transferred > 0; + public bool IsTransferred => Transferred == Total; + public abstract long Total { get; set; } + public long Transferred { get; set; } = 0; + + public override string ToString() + { + return Hash; + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/Models/ProgressableStreamContent.cs b/MareSynchronos/WebAPI/Files/Models/ProgressableStreamContent.cs new file mode 100644 index 0000000..7283a2b --- /dev/null +++ b/MareSynchronos/WebAPI/Files/Models/ProgressableStreamContent.cs @@ -0,0 +1,93 @@ +using System.Net; + +namespace MareSynchronos.WebAPI.Files.Models; + +public class ProgressableStreamContent : StreamContent +{ + private const int _defaultBufferSize = 4096; + private readonly int _bufferSize; + private readonly IProgress? _progress; + private readonly Stream _streamToWrite; + private bool _contentConsumed; + + public ProgressableStreamContent(Stream streamToWrite, IProgress? downloader) + : this(streamToWrite, _defaultBufferSize, downloader) + { + } + + public ProgressableStreamContent(Stream streamToWrite, int bufferSize, IProgress? progress) + : base(streamToWrite, bufferSize) + { + if (streamToWrite == null) + { + throw new ArgumentNullException(nameof(streamToWrite)); + } + + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + + _streamToWrite = streamToWrite; + _bufferSize = bufferSize; + _progress = progress; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _streamToWrite.Dispose(); + } + + base.Dispose(disposing); + } + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) + { + PrepareContent(); + + var buffer = new byte[_bufferSize]; + var size = _streamToWrite.Length; + var uploaded = 0; + + using (_streamToWrite) + { + while (true) + { + var length = await _streamToWrite.ReadAsync(buffer).ConfigureAwait(false); + if (length <= 0) + { + break; + } + + uploaded += length; + _progress?.Report(new UploadProgress(uploaded, size)); + await stream.WriteAsync(buffer.AsMemory(0, length)).ConfigureAwait(false); + } + } + } + + protected override bool TryComputeLength(out long length) + { + length = _streamToWrite.Length; + return true; + } + + private void PrepareContent() + { + if (_contentConsumed) + { + if (_streamToWrite.CanSeek) + { + _streamToWrite.Position = 0; + } + else + { + throw new InvalidOperationException("The stream has already been read."); + } + } + + _contentConsumed = true; + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/Models/UploadFileTransfer.cs b/MareSynchronos/WebAPI/Files/Models/UploadFileTransfer.cs new file mode 100644 index 0000000..fab2efc --- /dev/null +++ b/MareSynchronos/WebAPI/Files/Models/UploadFileTransfer.cs @@ -0,0 +1,13 @@ +using MareSynchronos.API.Dto.Files; + +namespace MareSynchronos.WebAPI.Files.Models; + +public class UploadFileTransfer : FileTransfer +{ + public UploadFileTransfer(UploadFileDto dto) : base(dto) + { + } + + public string LocalFile { get; set; } = string.Empty; + public override long Total { get; set; } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/Models/UploadProgress.cs b/MareSynchronos/WebAPI/Files/Models/UploadProgress.cs new file mode 100644 index 0000000..f3d64a9 --- /dev/null +++ b/MareSynchronos/WebAPI/Files/Models/UploadProgress.cs @@ -0,0 +1,3 @@ +namespace MareSynchronos.WebAPI.Files.Models; + +public record UploadProgress(long Uploaded, long Size); \ No newline at end of file diff --git a/MareSynchronos/WebAPI/Files/ThrottledStream.cs b/MareSynchronos/WebAPI/Files/ThrottledStream.cs new file mode 100644 index 0000000..a3b5c48 --- /dev/null +++ b/MareSynchronos/WebAPI/Files/ThrottledStream.cs @@ -0,0 +1,231 @@ +namespace MareSynchronos.WebAPI.Files +{ + /// + /// Class for streaming data with throttling support. + /// Borrowed from https://github.com/bezzad/Downloader + /// + internal class ThrottledStream : Stream + { + public static long Infinite => long.MaxValue; + private readonly Stream _baseStream; + private long _bandwidthLimit; + private readonly Bandwidth _bandwidth = new(); + private CancellationTokenSource _bandwidthChangeTokenSource = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The base stream. + /// The maximum bytes per second that can be transferred through the base stream. + /// Thrown when is a null reference. + /// Thrown when is a negative value. + public ThrottledStream(Stream baseStream, long bandwidthLimit) + { + if (bandwidthLimit < 0) + { + throw new ArgumentOutOfRangeException(nameof(bandwidthLimit), + bandwidthLimit, "The maximum number of bytes per second can't be negative."); + } + + _baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); + BandwidthLimit = bandwidthLimit; + } + + /// + /// Bandwidth Limit (in B/s) + /// + /// The maximum bytes per second. + public long BandwidthLimit + { + get => _bandwidthLimit; + set + { + if (_bandwidthLimit == value) return; + _bandwidthLimit = value <= 0 ? Infinite : value; + _bandwidth.BandwidthLimit = _bandwidthLimit; + _bandwidthChangeTokenSource.Cancel(); + _bandwidthChangeTokenSource.Dispose(); + _bandwidthChangeTokenSource = new(); + } + } + + /// + public override bool CanRead => _baseStream.CanRead; + + /// + public override bool CanSeek => _baseStream.CanSeek; + + /// + public override bool CanWrite => _baseStream.CanWrite; + + /// + public override long Length => _baseStream.Length; + + /// + public override long Position + { + get => _baseStream.Position; + set => _baseStream.Position = value; + } + + /// + public override void Flush() + { + _baseStream.Flush(); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + return _baseStream.Seek(offset, origin); + } + + /// + public override void SetLength(long value) + { + _baseStream.SetLength(value); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + Throttle(count).Wait(); + return _baseStream.Read(buffer, offset, count); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, + CancellationToken cancellationToken) + { + await Throttle(count, cancellationToken).ConfigureAwait(false); +#pragma warning disable CA1835 + return await _baseStream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); +#pragma warning restore CA1835 + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + await Throttle(buffer.Length, cancellationToken).ConfigureAwait(false); + return await _baseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + Throttle(count).Wait(); + _baseStream.Write(buffer, offset, count); + } + + /// + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await Throttle(count, cancellationToken).ConfigureAwait(false); +#pragma warning disable CA1835 + await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); +#pragma warning restore CA1835 + } + + /// + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + await Throttle(buffer.Length, cancellationToken).ConfigureAwait(false); + await _baseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + } + + public override void Close() + { + _baseStream.Close(); + base.Close(); + } + + private async Task Throttle(int transmissionVolume, CancellationToken token = default) + { + // Make sure the buffer isn't empty. + if (BandwidthLimit > 0 && transmissionVolume > 0) + { + // Calculate the time to sleep. + _bandwidth.CalculateSpeed(transmissionVolume); + await Sleep(_bandwidth.PopSpeedRetrieveTime(), token).ConfigureAwait(false); + } + } + + private async Task Sleep(int time, CancellationToken token = default) + { + try + { + if (time > 0) + { + var bandWidthtoken = _bandwidthChangeTokenSource.Token; + var linked = CancellationTokenSource.CreateLinkedTokenSource(token, bandWidthtoken).Token; + await Task.Delay(time, linked).ConfigureAwait(false); + } + } + catch (TaskCanceledException) + { + // ignore + } + } + + /// + public override string ToString() + { + return _baseStream?.ToString() ?? string.Empty; + } + + private sealed class Bandwidth + { + private long _count; + private int _lastSecondCheckpoint; + private long _lastTransferredBytesCount; + private int _speedRetrieveTime; + public double Speed { get; private set; } + public double AverageSpeed { get; private set; } + public long BandwidthLimit { get; set; } + + public Bandwidth() + { + BandwidthLimit = long.MaxValue; + Reset(); + } + + public void CalculateSpeed(long receivedBytesCount) + { + int elapsedTime = Environment.TickCount - _lastSecondCheckpoint + 1; + receivedBytesCount = Interlocked.Add(ref _lastTransferredBytesCount, receivedBytesCount); + double momentSpeed = receivedBytesCount * 1000 / (double)elapsedTime; // B/s + + if (1000 < elapsedTime) + { + Speed = momentSpeed; + AverageSpeed = ((AverageSpeed * _count) + Speed) / (_count + 1); + _count++; + SecondCheckpoint(); + } + + if (momentSpeed >= BandwidthLimit) + { + var expectedTime = receivedBytesCount * 1000 / BandwidthLimit; + Interlocked.Add(ref _speedRetrieveTime, (int)expectedTime - elapsedTime); + } + } + + public int PopSpeedRetrieveTime() + { + return Interlocked.Exchange(ref _speedRetrieveTime, 0); + } + + public void Reset() + { + SecondCheckpoint(); + _count = 0; + Speed = 0; + AverageSpeed = 0; + } + + private void SecondCheckpoint() + { + Interlocked.Exchange(ref _lastSecondCheckpoint, Environment.TickCount); + Interlocked.Exchange(ref _lastTransferredBytesCount, 0); + } + } + } +} diff --git a/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs b/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs new file mode 100644 index 0000000..08862d3 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -0,0 +1,116 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.User; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; +using System.Text; + +namespace MareSynchronos.WebAPI; + +public partial class ApiController +{ + public async Task PushCharacterData(CharacterData data, List visibleCharacters) + { + if (!IsConnected) return; + + try + { + Logger.LogDebug("Pushing Character data {hash} to {visible}", data.DataHash, string.Join(", ", visibleCharacters.Select(v => v.AliasOrUID))); + await PushCharacterDataInternal(data, [.. visibleCharacters]).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Logger.LogDebug("Upload operation was cancelled"); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error during upload of files"); + } + } + + public async Task UserAddPair(UserDto user) + { + if (!IsConnected) return; + await _mareHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false); + } + + public async Task UserChatSendMsg(UserDto user, ChatMessage message) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(UserChatSendMsg), user, message).ConfigureAwait(false); + } + + public async Task UserDelete() + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(UserDelete)).ConfigureAwait(false); + await CreateConnections().ConfigureAwait(false); + } + + public async Task> UserGetOnlinePairs() + { + return await _mareHub!.InvokeAsync>(nameof(UserGetOnlinePairs)).ConfigureAwait(false); + } + + public async Task> UserGetPairedClients() + { + return await _mareHub!.InvokeAsync>(nameof(UserGetPairedClients)).ConfigureAwait(false); + } + + public async Task UserGetProfile(UserDto dto) + { + if (!IsConnected) return new UserProfileDto(dto.User, false, null, null, null); + return await _mareHub!.InvokeAsync(nameof(UserGetProfile), dto).ConfigureAwait(false); + } + + public async Task UserPushData(UserCharaDataMessageDto dto) + { + try + { + await _mareHub!.InvokeAsync(nameof(UserPushData), dto).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to Push character data"); + } + } + + public async Task UserRemovePair(UserDto userDto) + { + if (!IsConnected) return; + await _mareHub!.SendAsync(nameof(UserRemovePair), userDto).ConfigureAwait(false); + } + + public async Task UserReportProfile(UserProfileReportDto userDto) + { + if (!IsConnected) return; + await _mareHub!.SendAsync(nameof(UserReportProfile), userDto).ConfigureAwait(false); + } + + public async Task UserSetPairPermissions(UserPermissionsDto userPermissions) + { + await _mareHub!.SendAsync(nameof(UserSetPairPermissions), userPermissions).ConfigureAwait(false); + } + + public async Task UserSetProfile(UserProfileDto userDescription) + { + if (!IsConnected) return; + await _mareHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false); + } + + private async Task PushCharacterDataInternal(CharacterData character, List visibleCharacters) + { + Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID))); + StringBuilder sb = new(); + foreach (var kvp in character.FileReplacements.ToList()) + { + sb.AppendLine($"FileReplacements for {kvp.Key}: {kvp.Value.Count}"); + } + foreach (var item in character.GlamourerData) + { + sb.AppendLine($"GlamourerData for {item.Key}: {!string.IsNullOrEmpty(item.Value)}"); + } + Logger.LogDebug("Chara data contained: {nl} {data}", Environment.NewLine, sb.ToString()); + + await UserPushData(new(visibleCharacters, character)).ConfigureAwait(false); + } +} diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs new file mode 100644 index 0000000..662c0e6 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Callbacks.cs @@ -0,0 +1,405 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.Chat; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services.Mediator; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; +using static FFXIVClientStructs.FFXIV.Client.Game.UI.MapMarkerData.Delegates; + +namespace MareSynchronos.WebAPI; + +public partial class ApiController +{ + public Task Client_DownloadReady(Guid requestId) + { + Logger.LogDebug("Server sent {requestId} ready", requestId); + Mediator.Publish(new DownloadReadyMessage(requestId)); + return Task.CompletedTask; + } + + public Task Client_GroupChangePermissions(GroupPermissionDto groupPermission) + { + Logger.LogTrace("Client_GroupChangePermissions: {perm}", groupPermission); + ExecuteSafely(() => _pairManager.SetGroupPermissions(groupPermission)); + return Task.CompletedTask; + } + + public Task Client_GroupChatMsg(GroupChatMsgDto groupChatMsgDto) + { + Logger.LogDebug("Client_GroupChatMsg: {msg}", groupChatMsgDto.Message); + Mediator.Publish(new GroupChatMsgMessage(groupChatMsgDto.Group, groupChatMsgDto.Message)); + return Task.CompletedTask; + } + + public Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto dto) + { + Logger.LogTrace("Client_GroupPairChangePermissions: {dto}", dto); + ExecuteSafely(() => + { + if (string.Equals(dto.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupUserPermissions(dto); + else _pairManager.SetGroupPairUserPermissions(dto); + }); + return Task.CompletedTask; + } + + public Task Client_GroupDelete(GroupDto groupDto) + { + Logger.LogTrace("Client_GroupDelete: {dto}", groupDto); + ExecuteSafely(() => _pairManager.RemoveGroup(groupDto.Group)); + return Task.CompletedTask; + } + + public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto userInfo) + { + Logger.LogTrace("Client_GroupPairChangeUserInfo: {dto}", userInfo); + ExecuteSafely(() => + { + if (string.Equals(userInfo.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupStatusInfo(userInfo); + else _pairManager.SetGroupPairStatusInfo(userInfo); + }); + return Task.CompletedTask; + } + + public Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto) + { + Logger.LogTrace("Client_GroupPairJoined: {dto}", groupPairInfoDto); + ExecuteSafely(() => _pairManager.AddGroupPair(groupPairInfoDto)); + return Task.CompletedTask; + } + + public Task Client_GroupPairLeft(GroupPairDto groupPairDto) + { + Logger.LogTrace("Client_GroupPairLeft: {dto}", groupPairDto); + ExecuteSafely(() => _pairManager.RemoveGroupPair(groupPairDto)); + return Task.CompletedTask; + } + + public Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo) + { + Logger.LogTrace("Client_GroupSendFullInfo: {dto}", groupInfo); + ExecuteSafely(() => _pairManager.AddGroup(groupInfo)); + return Task.CompletedTask; + } + + public Task Client_GroupSendInfo(GroupInfoDto groupInfo) + { + Logger.LogTrace("Client_GroupSendInfo: {dto}", groupInfo); + ExecuteSafely(() => _pairManager.SetGroupInfo(groupInfo)); + return Task.CompletedTask; + } + + public Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message) + { + switch (messageSeverity) + { + case MessageSeverity.Error: + Mediator.Publish(new NotificationMessage("Warning from " + _serverManager.CurrentServer!.ServerName, message, NotificationType.Error, TimeSpan.FromSeconds(7.5))); + break; + + case MessageSeverity.Warning: + Mediator.Publish(new NotificationMessage("Warning from " + _serverManager.CurrentServer!.ServerName, message, NotificationType.Warning, TimeSpan.FromSeconds(7.5))); + break; + + case MessageSeverity.Information: + if (_doNotNotifyOnNextInfo) + { + _doNotNotifyOnNextInfo = false; + break; + } + Mediator.Publish(new NotificationMessage("Info from " + _serverManager.CurrentServer!.ServerName, message, NotificationType.Info, TimeSpan.FromSeconds(5))); + break; + } + + return Task.CompletedTask; + } + + public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) + { + SystemInfoDto = systemInfo; + return Task.CompletedTask; + } + + public Task Client_UserAddClientPair(UserPairDto dto) + { + Logger.LogDebug("Client_UserAddClientPair: {dto}", dto); + ExecuteSafely(() => _pairManager.AddUserPair(dto, addToLastAddedUser: true)); + return Task.CompletedTask; + } + + public Task Client_UserChatMsg(UserChatMsgDto chatMsgDto) + { + Logger.LogDebug("Client_UserChatMsg: {msg}", chatMsgDto.Message); + Mediator.Publish(new UserChatMsgMessage(chatMsgDto.Message)); + return Task.CompletedTask; + } + + public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto) + { + Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User); + ExecuteSafely(() => _pairManager.ReceiveCharaData(dataDto)); + return Task.CompletedTask; + } + + public Task Client_UserReceiveUploadStatus(UserDto dto) + { + Logger.LogTrace("Client_UserReceiveUploadStatus: {dto}", dto); + ExecuteSafely(() => _pairManager.ReceiveUploadStatus(dto)); + return Task.CompletedTask; + } + + public Task Client_UserRemoveClientPair(UserDto dto) + { + Logger.LogDebug("Client_UserRemoveClientPair: {dto}", dto); + ExecuteSafely(() => _pairManager.RemoveUserPair(dto)); + return Task.CompletedTask; + } + + public Task Client_UserSendOffline(UserDto dto) + { + Logger.LogDebug("Client_UserSendOffline: {dto}", dto); + ExecuteSafely(() => _pairManager.MarkPairOffline(dto.User)); + return Task.CompletedTask; + } + + public Task Client_UserSendOnline(OnlineUserIdentDto dto) + { + Logger.LogDebug("Client_UserSendOnline: {dto}", dto); + ExecuteSafely(() => _pairManager.MarkPairOnline(dto)); + return Task.CompletedTask; + } + + public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto) + { + Logger.LogDebug("Client_UserUpdateOtherPairPermissions: {dto}", dto); + ExecuteSafely(() => _pairManager.UpdatePairPermissions(dto)); + return Task.CompletedTask; + } + + public Task Client_UserUpdateProfile(UserDto dto) + { + Logger.LogDebug("Client_UserUpdateProfile: {dto}", dto); + ExecuteSafely(() => Mediator.Publish(new ClearProfileDataMessage(dto.User))); + return Task.CompletedTask; + } + + public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto) + { + Logger.LogDebug("Client_UserUpdateSelfPairPermissions: {dto}", dto); + ExecuteSafely(() => _pairManager.UpdateSelfPairPermissions(dto)); + return Task.CompletedTask; + } + + public Task Client_GposeLobbyJoin(UserData userData) + { + Logger.LogDebug("Client_GposeLobbyJoin: {dto}", userData); + ExecuteSafely(() => Mediator.Publish(new GposeLobbyUserJoin(userData))); + return Task.CompletedTask; + } + + public Task Client_GposeLobbyLeave(UserData userData) + { + Logger.LogDebug("Client_GposeLobbyLeave: {dto}", userData); + ExecuteSafely(() => Mediator.Publish(new GPoseLobbyUserLeave(userData))); + return Task.CompletedTask; + } + + public Task Client_GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto) + { + Logger.LogDebug("Client_GposeLobbyPushCharacterData: {dto}", charaDownloadDto.Uploader); + ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveCharaData(charaDownloadDto))); + return Task.CompletedTask; + } + + public Task Client_GposeLobbyPushPoseData(UserData userData, PoseData poseData) + { + Logger.LogDebug("Client_GposeLobbyPushPoseData: {dto}", userData); + ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceivePoseData(userData, poseData))); + return Task.CompletedTask; + } + + public Task Client_GposeLobbyPushWorldData(UserData userData, WorldData worldData) + { + //Logger.LogDebug("Client_GposeLobbyPushWorldData: {dto}", userData); + ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData))); + return Task.CompletedTask; + } + + public void OnDownloadReady(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_DownloadReady), act); + } + + public void OnGroupChangePermissions(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupChangePermissions), act); + } + + public void OnGroupChatMsg(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupChatMsg), act); + } + + public void OnGroupPairChangePermissions(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupPairChangePermissions), act); + } + + public void OnGroupDelete(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupDelete), act); + } + + public void OnGroupPairChangeUserInfo(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupPairChangeUserInfo), act); + } + + public void OnGroupPairJoined(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupPairJoined), act); + } + + public void OnGroupPairLeft(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupPairLeft), act); + } + + public void OnGroupSendFullInfo(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupSendFullInfo), act); + } + + public void OnGroupSendInfo(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GroupSendInfo), act); + } + + public void OnReceiveServerMessage(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_ReceiveServerMessage), act); + } + + public void OnUpdateSystemInfo(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UpdateSystemInfo), act); + } + + public void OnUserAddClientPair(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserAddClientPair), act); + } + + public void OnUserChatMsg(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserChatMsg), act); + } + + public void OnUserReceiveCharacterData(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserReceiveCharacterData), act); + } + + public void OnUserReceiveUploadStatus(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserReceiveUploadStatus), act); + } + + public void OnUserRemoveClientPair(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserRemoveClientPair), act); + } + + public void OnUserSendOffline(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserSendOffline), act); + } + + public void OnUserSendOnline(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserSendOnline), act); + } + + public void OnUserUpdateOtherPairPermissions(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserUpdateOtherPairPermissions), act); + } + + public void OnUserUpdateProfile(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserUpdateProfile), act); + } + + public void OnUserUpdateSelfPairPermissions(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_UserUpdateSelfPairPermissions), act); + } + + public void OnGposeLobbyJoin(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GposeLobbyJoin), act); + } + + public void OnGposeLobbyLeave(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GposeLobbyLeave), act); + } + + public void OnGposeLobbyPushCharacterData(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GposeLobbyPushCharacterData), act); + } + + public void OnGposeLobbyPushPoseData(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GposeLobbyPushPoseData), act); + } + + public void OnGposeLobbyPushWorldData(Action act) + { + if (_initialized) return; + _mareHub!.On(nameof(Client_GposeLobbyPushWorldData), act); + } + + private void ExecuteSafely(Action act) + { + try + { + act(); + } + catch (Exception ex) + { + Logger.LogCritical(ex, "Error on executing safely"); + } + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.CharaData.cs b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.CharaData.cs new file mode 100644 index 0000000..eaa95e8 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.CharaData.cs @@ -0,0 +1,228 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.CharaData; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.WebAPI; +public partial class ApiController +{ + public async Task CharaDataCreate() + { + if (!IsConnected) return null; + + try + { + Logger.LogDebug("Creating new Character Data"); + return await _mareHub!.InvokeAsync(nameof(CharaDataCreate)).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to create new character data"); + return null; + } + } + + public async Task CharaDataUpdate(CharaDataUpdateDto updateDto) + { + if (!IsConnected) return null; + + try + { + Logger.LogDebug("Updating chara data for {id}", updateDto.Id); + return await _mareHub!.InvokeAsync(nameof(CharaDataUpdate), updateDto).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to update chara data for {id}", updateDto.Id); + return null; + } + } + + public async Task CharaDataDelete(string id) + { + if (!IsConnected) return false; + + try + { + Logger.LogDebug("Deleting chara data for {id}", id); + return await _mareHub!.InvokeAsync(nameof(CharaDataDelete), id).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to delete chara data for {id}", id); + return false; + } + } + + public async Task CharaDataGetMetainfo(string id) + { + if (!IsConnected) return null; + + try + { + Logger.LogDebug("Getting metainfo for chara data {id}", id); + return await _mareHub!.InvokeAsync(nameof(CharaDataGetMetainfo), id).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to get meta info for chara data {id}", id); + return null; + } + } + + public async Task CharaDataAttemptRestore(string id) + { + if (!IsConnected) return null; + + try + { + Logger.LogDebug("Attempting to restore chara data {id}", id); + return await _mareHub!.InvokeAsync(nameof(CharaDataAttemptRestore), id).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to restore chara data for {id}", id); + return null; + } + } + + public async Task> CharaDataGetOwn() + { + if (!IsConnected) return []; + + try + { + Logger.LogDebug("Getting all own chara data"); + return await _mareHub!.InvokeAsync>(nameof(CharaDataGetOwn)).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to get own chara data"); + return []; + } + } + + public async Task> CharaDataGetShared() + { + if (!IsConnected) return []; + + try + { + Logger.LogDebug("Getting all own chara data"); + return await _mareHub!.InvokeAsync>(nameof(CharaDataGetShared)).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to get shared chara data"); + return []; + } + } + + public async Task CharaDataDownload(string id) + { + if (!IsConnected) return null; + + try + { + Logger.LogDebug("Getting download chara data for {id}", id); + return await _mareHub!.InvokeAsync(nameof(CharaDataDownload), id).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to get download chara data for {id}", id); + return null; + } + } + + public async Task GposeLobbyCreate() + { + if (!IsConnected) return string.Empty; + + try + { + Logger.LogDebug("Creating GPose Lobby"); + return await _mareHub!.InvokeAsync(nameof(GposeLobbyCreate)).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to create GPose lobby"); + return string.Empty; + } + } + + public async Task GposeLobbyLeave() + { + if (!IsConnected) return true; + + try + { + Logger.LogDebug("Leaving current GPose Lobby"); + return await _mareHub!.InvokeAsync(nameof(GposeLobbyLeave)).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to leave GPose lobby"); + return false; + } + } + + public async Task> GposeLobbyJoin(string lobbyId) + { + if (!IsConnected) return []; + + try + { + Logger.LogDebug("Joining GPose Lobby {id}", lobbyId); + return await _mareHub!.InvokeAsync>(nameof(GposeLobbyJoin), lobbyId).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to join GPose lobby {id}", lobbyId); + return []; + } + } + + public async Task GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto) + { + if (!IsConnected) return; + + try + { + Logger.LogDebug("Sending Chara Data to GPose Lobby"); + await _mareHub!.InvokeAsync(nameof(GposeLobbyPushCharacterData), charaDownloadDto).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to send Chara Data to GPose lobby"); + } + } + + public async Task GposeLobbyPushPoseData(PoseData poseData) + { + if (!IsConnected) return; + + try + { + Logger.LogDebug("Sending Pose Data to GPose Lobby"); + await _mareHub!.InvokeAsync(nameof(GposeLobbyPushPoseData), poseData).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to send Pose Data to GPose lobby"); + } + } + + public async Task GposeLobbyPushWorldData(WorldData worldData) + { + if (!IsConnected) return; + + try + { + await _mareHub!.InvokeAsync(nameof(GposeLobbyPushWorldData), worldData).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to send World Data to GPose lobby"); + } + } +} diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs new file mode 100644 index 0000000..7a0f54c --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/ApiController.Functions.Groups.cs @@ -0,0 +1,128 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.WebAPI.SignalR.Utils; +using Microsoft.AspNetCore.SignalR.Client; + +namespace MareSynchronos.WebAPI; + +public partial class ApiController +{ + public async Task GroupBanUser(GroupPairDto dto, string reason) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupBanUser), dto, reason).ConfigureAwait(false); + } + + public async Task GroupChangeGroupPermissionState(GroupPermissionDto dto) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupChangeGroupPermissionState), dto).ConfigureAwait(false); + } + + public async Task GroupChangeIndividualPermissionState(GroupPairUserPermissionDto dto) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupChangeIndividualPermissionState), dto).ConfigureAwait(false); + } + + public async Task GroupChangeOwnership(GroupPairDto groupPair) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupChangeOwnership), groupPair).ConfigureAwait(false); + } + + public async Task GroupChangePassword(GroupPasswordDto groupPassword) + { + CheckConnection(); + return await _mareHub!.InvokeAsync(nameof(GroupChangePassword), groupPassword).ConfigureAwait(false); + } + + public async Task GroupChatSendMsg(GroupDto group, ChatMessage message) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupChatSendMsg), group, message).ConfigureAwait(false); + } + + public async Task GroupClear(GroupDto group) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false); + } + + public async Task GroupCreate() + { + CheckConnection(); + return await _mareHub!.InvokeAsync(nameof(GroupCreate)).ConfigureAwait(false); + } + + public async Task> GroupCreateTempInvite(GroupDto group, int amount) + { + CheckConnection(); + return await _mareHub!.InvokeAsync>(nameof(GroupCreateTempInvite), group, amount).ConfigureAwait(false); + } + + public async Task GroupDelete(GroupDto group) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupDelete), group).ConfigureAwait(false); + } + + public async Task> GroupGetBannedUsers(GroupDto group) + { + CheckConnection(); + return await _mareHub!.InvokeAsync>(nameof(GroupGetBannedUsers), group).ConfigureAwait(false); + } + + public async Task GroupJoin(GroupPasswordDto passwordedGroup) + { + CheckConnection(); + return await _mareHub!.InvokeAsync(nameof(GroupJoin), passwordedGroup).ConfigureAwait(false); + } + + public async Task GroupLeave(GroupDto group) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupLeave), group).ConfigureAwait(false); + } + + public async Task GroupRemoveUser(GroupPairDto groupPair) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupRemoveUser), groupPair).ConfigureAwait(false); + } + + public async Task GroupSetUserInfo(GroupPairUserInfoDto groupPair) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupSetUserInfo), groupPair).ConfigureAwait(false); + } + + public async Task GroupPrune(GroupDto group, int days, bool execute) + { + CheckConnection(); + return await _mareHub!.InvokeAsync(nameof(GroupPrune), group, days, execute).ConfigureAwait(false); + } + + public async Task> GroupsGetAll() + { + CheckConnection(); + return await _mareHub!.InvokeAsync>(nameof(GroupsGetAll)).ConfigureAwait(false); + } + + public async Task> GroupsGetUsersInGroup(GroupDto group) + { + CheckConnection(); + return await _mareHub!.InvokeAsync>(nameof(GroupsGetUsersInGroup), group).ConfigureAwait(false); + } + + public async Task GroupUnbanUser(GroupPairDto groupPair) + { + CheckConnection(); + await _mareHub!.SendAsync(nameof(GroupUnbanUser), groupPair).ConfigureAwait(false); + } + + private void CheckConnection() + { + if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected"); + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.cs b/MareSynchronos/WebAPI/SignalR/ApiController.cs new file mode 100644 index 0000000..8ddb14a --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/ApiController.cs @@ -0,0 +1,482 @@ +using Dalamud.Utility; +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Extensions; +using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.User; +using MareSynchronos.API.SignalR; +using MareSynchronos.MareConfiguration; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.PlayerData.Pairs; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.WebAPI.SignalR; +using MareSynchronos.WebAPI.SignalR.Utils; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; +using System.Reflection; + +namespace MareSynchronos.WebAPI; + +#pragma warning disable MA0040 +public sealed partial class ApiController : DisposableMediatorSubscriberBase, IMareHubClient +{ + public const string SnowcloakServer = "Snowcloak Main Server"; + public const string SnowcloakServiceUri = "wss://hub.snowcloak-sync.com"; + public const string SnowcloakServiceApiUri = "wss://hub.snowcloak-sync.com/"; + public const string SnowcloakServiceHubUri = "wss://hub.snowcloak-sync.com/mare"; + + private readonly DalamudUtilService _dalamudUtil; + private readonly HubFactory _hubFactory; + private readonly PairManager _pairManager; + private readonly ServerConfigurationManager _serverManager; + private readonly TokenProvider _tokenProvider; + private CancellationTokenSource _connectionCancellationTokenSource; + private ConnectionDto? _connectionDto; + private bool _doNotNotifyOnNextInfo = false; + private CancellationTokenSource? _healthCheckTokenSource = new(); + private bool _initialized; + private HubConnection? _mareHub; + private ServerState _serverState; + private CensusUpdateMessage? _lastCensus; + + public ApiController(ILogger logger, HubFactory hubFactory, DalamudUtilService dalamudUtil, + PairManager pairManager, ServerConfigurationManager serverManager, MareMediator mediator, + TokenProvider tokenProvider) : base(logger, mediator) + { + _hubFactory = hubFactory; + _dalamudUtil = dalamudUtil; + _pairManager = pairManager; + _serverManager = serverManager; + _tokenProvider = tokenProvider; + _connectionCancellationTokenSource = new CancellationTokenSource(); + + Mediator.Subscribe(this, (_) => DalamudUtilOnLogIn()); + Mediator.Subscribe(this, (_) => DalamudUtilOnLogOut()); + Mediator.Subscribe(this, (msg) => MareHubOnClosed(msg.Exception)); + Mediator.Subscribe(this, (msg) => _ = MareHubOnReconnected()); + Mediator.Subscribe(this, (msg) => MareHubOnReconnecting(msg.Exception)); + Mediator.Subscribe(this, (msg) => _ = CyclePause(msg.UserData)); + Mediator.Subscribe(this, (msg) => _lastCensus = msg); + Mediator.Subscribe(this, (msg) => _ = Pause(msg.UserData)); + + ServerState = ServerState.Offline; + + if (_dalamudUtil.IsLoggedIn) + { + DalamudUtilOnLogIn(); + } + } + + public string AuthFailureMessage { get; private set; } = string.Empty; + + public Version CurrentClientVersion => _connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0); + + public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty; + + public bool IsConnected => ServerState == ServerState.Connected; + + public bool IsCurrentVersion => (Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0)) >= (_connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0, 0)); + + public int OnlineUsers => SystemInfoDto.OnlineUsers; + + public bool ServerAlive => ServerState is ServerState.Connected or ServerState.RateLimited or ServerState.Unauthorized or ServerState.Disconnected; + + public ServerInfo ServerInfo => _connectionDto?.ServerInfo ?? new ServerInfo(); + + public ServerState ServerState + { + get => _serverState; + private set + { + Logger.LogDebug("New ServerState: {value}, prev ServerState: {_serverState}", value, _serverState); + _serverState = value; + } + } + + public SystemInfoDto SystemInfoDto { get; private set; } = new(); + + public string UID => _connectionDto?.User.UID ?? string.Empty; + + public async Task CheckClientHealth() + { + return await _mareHub!.InvokeAsync(nameof(CheckClientHealth)).ConfigureAwait(false); + } + + public async Task CreateConnections() + { + Logger.LogDebug("CreateConnections called"); + + if (_serverManager.CurrentServer?.FullPause ?? true) + { + Logger.LogInformation("Not recreating Connection, paused"); + _connectionDto = null; + await StopConnection(ServerState.Disconnected).ConfigureAwait(false); + _connectionCancellationTokenSource?.Cancel(); + return; + } + + var secretKey = _serverManager.GetSecretKey(out bool multi); + if (multi) + { + Logger.LogWarning("Multiple secret keys for current character"); + _connectionDto = null; + 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 Mare.", + NotificationType.Error)); + await StopConnection(ServerState.MultiChara).ConfigureAwait(false); + _connectionCancellationTokenSource?.Cancel(); + return; + } + + if (secretKey == null) + { + Logger.LogWarning("No secret key set for current character"); + _connectionDto = null; + await StopConnection(ServerState.NoSecretKey).ConfigureAwait(false); + _connectionCancellationTokenSource?.Cancel(); + return; + } + + await StopConnection(ServerState.Disconnected).ConfigureAwait(false); + + Logger.LogInformation("Recreating Connection"); + Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Informational, + $"Starting Connection to {_serverManager.CurrentServer.ServerName}"))); + + _connectionCancellationTokenSource?.Cancel(); + _connectionCancellationTokenSource?.Dispose(); + _connectionCancellationTokenSource = new CancellationTokenSource(); + var token = _connectionCancellationTokenSource.Token; + while (ServerState is not ServerState.Connected && !token.IsCancellationRequested) + { + AuthFailureMessage = string.Empty; + + await StopConnection(ServerState.Disconnected).ConfigureAwait(false); + ServerState = ServerState.Connecting; + + try + { + Logger.LogDebug("Building connection"); + + try + { + await _tokenProvider.GetOrUpdateToken(token).ConfigureAwait(false); + } + catch (MareAuthFailureException ex) + { + AuthFailureMessage = ex.Reason; + throw new HttpRequestException("Error during authentication", ex, System.Net.HttpStatusCode.Unauthorized); + } + + while (!await _dalamudUtil.GetIsPlayerPresentAsync().ConfigureAwait(false) && !token.IsCancellationRequested) + { + Logger.LogDebug("Player not loaded in yet, waiting"); + await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); + } + + if (token.IsCancellationRequested) break; + + _mareHub = await _hubFactory.GetOrCreate(token).ConfigureAwait(false); + InitializeApiHooks(); + + await _mareHub.StartAsync(token).ConfigureAwait(false); + + _connectionDto = await GetConnectionDto().ConfigureAwait(false); + + ServerState = ServerState.Connected; + + var currentClientVer = Assembly.GetExecutingAssembly().GetName().Version!; + + if (_connectionDto.ServerVersion != IMareHub.ApiVersion) + { + if (_connectionDto.CurrentClientVersion > currentClientVer) + { + Mediator.Publish(new NotificationMessage("Client incompatible", + $"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " + + $"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " + + $"This client version is incompatible and will not be able to connect. Please update your Snowcloak client.", + NotificationType.Error)); + } + await StopConnection(ServerState.VersionMisMatch).ConfigureAwait(false); + return; + } + + if (_connectionDto.CurrentClientVersion > currentClientVer) + { + Mediator.Publish(new NotificationMessage("Client outdated", + $"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " + + $"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " + + $"Please keep your Snowcloak client up-to-date.", + NotificationType.Warning, TimeSpan.FromSeconds(15))); + } + + if (_dalamudUtil.HasModifiedGameFiles) + { + Logger.LogWarning("Detected modified game files on connection"); +#if false + Mediator.Publish(new NotificationMessage("Modified Game Files detected", + "Dalamud has reported modified game files in your FFXIV installation. " + + "You will be able to connect, but the synchronization functionality might be (partially) broken. " + + "Exit the game and repair it through XIVLauncher to get rid of this message.", + NotificationType.Error, TimeSpan.FromSeconds(15))); +#endif + } + + await LoadIninitialPairs().ConfigureAwait(false); + await LoadOnlinePairs().ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Logger.LogWarning("Connection attempt cancelled"); + return; + } + catch (HttpRequestException ex) + { + Logger.LogWarning(ex, "HttpRequestException on Connection"); + + if (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + await StopConnection(ServerState.Unauthorized).ConfigureAwait(false); + return; + } + + ServerState = ServerState.Reconnecting; + Logger.LogInformation("Failed to establish connection, retrying"); + await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false); + } + catch (InvalidOperationException ex) + { + Logger.LogWarning(ex, "InvalidOperationException on connection"); + await StopConnection(ServerState.Disconnected).ConfigureAwait(false); + return; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Exception on Connection"); + + Logger.LogInformation("Failed to establish connection, retrying"); + await Task.Delay(TimeSpan.FromSeconds(new Random().Next(5, 20)), token).ConfigureAwait(false); + } + } + } + + public Task CyclePause(UserData userData) + { + CancellationTokenSource cts = new(); + cts.CancelAfter(TimeSpan.FromSeconds(5)); + _ = 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) + { + await Task.Delay(250, cts.Token).ConfigureAwait(false); + Logger.LogTrace("Waiting for permissions change for {data}", userData); + } + perm.SetPaused(paused: false); + await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false); + }, cts.Token).ContinueWith((t) => cts.Dispose()); + + return Task.CompletedTask; + } + + public async Task Pause(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); + } + + public Task GetConnectionDto() => GetConnectionDto(true); + + public async Task GetConnectionDto(bool publishConnected = true) + { + var dto = await _mareHub!.InvokeAsync(nameof(GetConnectionDto)).ConfigureAwait(false); + if (publishConnected) Mediator.Publish(new ConnectedMessage(dto)); + return dto; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _healthCheckTokenSource?.Cancel(); + _ = Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false)); + _connectionCancellationTokenSource?.Cancel(); + } + + private async Task ClientHealthCheck(CancellationToken ct) + { + while (!ct.IsCancellationRequested && _mareHub != null) + { + await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false); + Logger.LogDebug("Checking Client Health State"); + _ = await CheckClientHealth().ConfigureAwait(false); + } + } + + private void DalamudUtilOnLogIn() + { + _ = Task.Run(() => CreateConnections()); + } + + private void DalamudUtilOnLogOut() + { + _ = Task.Run(async () => await StopConnection(ServerState.Disconnected).ConfigureAwait(false)); + ServerState = ServerState.Offline; + } + + private void InitializeApiHooks() + { + if (_mareHub == null) return; + + Logger.LogDebug("Initializing data"); + OnDownloadReady((guid) => _ = Client_DownloadReady(guid)); + OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg)); + OnUpdateSystemInfo((dto) => _ = Client_UpdateSystemInfo(dto)); + + OnUserSendOffline((dto) => _ = Client_UserSendOffline(dto)); + OnUserAddClientPair((dto) => _ = Client_UserAddClientPair(dto)); + OnUserReceiveCharacterData((dto) => _ = Client_UserReceiveCharacterData(dto)); + OnUserRemoveClientPair(dto => _ = Client_UserRemoveClientPair(dto)); + OnUserSendOnline(dto => _ = Client_UserSendOnline(dto)); + OnUserUpdateOtherPairPermissions(dto => _ = Client_UserUpdateOtherPairPermissions(dto)); + OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto)); + OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto)); + OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto)); + + OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto)); + OnGroupDelete((dto) => _ = Client_GroupDelete(dto)); + OnGroupPairChangeUserInfo((dto) => _ = Client_GroupPairChangeUserInfo(dto)); + OnGroupPairJoined((dto) => _ = Client_GroupPairJoined(dto)); + OnGroupPairLeft((dto) => _ = Client_GroupPairLeft(dto)); + OnGroupSendFullInfo((dto) => _ = Client_GroupSendFullInfo(dto)); + OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto)); + OnGroupPairChangePermissions((dto) => _ = Client_GroupPairChangePermissions(dto)); + + OnUserChatMsg((dto) => _ = Client_UserChatMsg(dto)); + OnGroupChatMsg((dto) => _ = Client_GroupChatMsg(dto)); + + OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto)); + OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto)); + OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto)); + OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data)); + OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data)); + + _healthCheckTokenSource?.Cancel(); + _healthCheckTokenSource?.Dispose(); + _healthCheckTokenSource = new CancellationTokenSource(); + _ = ClientHealthCheck(_healthCheckTokenSource.Token); + + _initialized = true; + } + + private async Task LoadIninitialPairs() + { + foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false)) + { + Logger.LogDebug("Individual Pair: {userPair}", userPair); + _pairManager.AddUserPair(userPair, addToLastAddedUser: false); + } + foreach (var entry in await GroupsGetAll().ConfigureAwait(false)) + { + Logger.LogDebug("Group: {entry}", entry); + _pairManager.AddGroup(entry); + } + foreach (var group in _pairManager.GroupPairs.Keys) + { + var users = await GroupsGetUsersInGroup(group).ConfigureAwait(false); + foreach (var user in users) + { + Logger.LogDebug("Group Pair: {user}", user); + _pairManager.AddGroupPair(user); + } + } + } + + private async Task LoadOnlinePairs() + { + foreach (var entry in await UserGetOnlinePairs().ConfigureAwait(false)) + { + Logger.LogDebug("Pair online: {pair}", entry); + _pairManager.MarkPairOnline(entry, sendNotif: false); + } + } + + private void MareHubOnClosed(Exception? arg) + { + _healthCheckTokenSource?.Cancel(); + Mediator.Publish(new DisconnectedMessage()); + ServerState = ServerState.Offline; + if (arg != null) + { + Logger.LogWarning(arg, "Connection closed"); + } + else + { + Logger.LogInformation("Connection closed"); + } + } + + private async Task MareHubOnReconnected() + { + ServerState = ServerState.Reconnecting; + try + { + InitializeApiHooks(); + _connectionDto = await GetConnectionDto(publishConnected: false).ConfigureAwait(false); + if (_connectionDto.ServerVersion != IMareHub.ApiVersion) + { + await StopConnection(ServerState.VersionMisMatch).ConfigureAwait(false); + return; + } + ServerState = ServerState.Connected; + await LoadIninitialPairs().ConfigureAwait(false); + await LoadOnlinePairs().ConfigureAwait(false); + Mediator.Publish(new ConnectedMessage(_connectionDto)); + } + catch (Exception ex) + { + Logger.LogCritical(ex, "Failure to obtain data after reconnection"); + await StopConnection(ServerState.Disconnected).ConfigureAwait(false); + } + } + + private void MareHubOnReconnecting(Exception? arg) + { + _doNotNotifyOnNextInfo = true; + _healthCheckTokenSource?.Cancel(); + ServerState = ServerState.Reconnecting; + Logger.LogWarning(arg, "Connection closed... Reconnecting"); + Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Warning, + $"Connection interrupted, reconnecting to {_serverManager.CurrentServer.ServerName}"))); + + } + + private async Task StopConnection(ServerState state) + { + ServerState = ServerState.Disconnecting; + + Logger.LogInformation("Stopping existing connection"); + await _hubFactory.DisposeHubAsync().ConfigureAwait(false); + + if (_mareHub is not null) + { + Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Informational, + $"Stopping existing connection to {_serverManager.CurrentServer.ServerName}"))); + + _initialized = false; + _healthCheckTokenSource?.Cancel(); + Mediator.Publish(new DisconnectedMessage()); + _mareHub = null; + _connectionDto = null; + } + + ServerState = state; + } +} +#pragma warning restore MA0040 \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/HubConnectionConfig.cs b/MareSynchronos/WebAPI/SignalR/HubConnectionConfig.cs new file mode 100644 index 0000000..f7eb825 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/HubConnectionConfig.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Http.Connections; +using System.Text.Json.Serialization; + +namespace MareSynchronos.WebAPI.SignalR; + +public record HubConnectionConfig +{ + [JsonPropertyName("api_url")] + public string ApiUrl { get; set; } = string.Empty; + + [JsonPropertyName("hub_url")] + public string HubUrl { get; set; } = string.Empty; + + private readonly bool? _skipNegotiation; + + [JsonPropertyName("skip_negotiation")] + public bool SkipNegotiation + { + get => _skipNegotiation ?? true; + init => _skipNegotiation = value; + } + + [JsonPropertyName("transports")] + public string[]? Transports { get; set; } + + [JsonIgnore] + public HttpTransportType TransportType + { + get + { + if (Transports == null || Transports.Length == 0) + return HttpTransportType.WebSockets; + + HttpTransportType result = HttpTransportType.None; + + foreach (var transport in Transports) + { + result |= transport.ToLowerInvariant() switch + { + "websockets" => HttpTransportType.WebSockets, + "serversentevents" => HttpTransportType.ServerSentEvents, + "longpolling" => HttpTransportType.LongPolling, + _ => HttpTransportType.None + }; + } + + return result; + } + } +} diff --git a/MareSynchronos/WebAPI/SignalR/HubFactory.cs b/MareSynchronos/WebAPI/SignalR/HubFactory.cs new file mode 100644 index 0000000..0c6b3bb --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/HubFactory.cs @@ -0,0 +1,228 @@ +using MareSynchronos.API.SignalR; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.WebAPI.SignalR.Utils; +using MessagePack; +using MessagePack.Resolvers; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Net.Http.Headers; +using System.Reflection; +using System.Text.Json; + +namespace MareSynchronos.WebAPI.SignalR; + +public class HubFactory : MediatorSubscriberBase +{ + private readonly ILoggerProvider _loggingProvider; + private readonly ServerConfigurationManager _serverConfigurationManager; + private readonly TokenProvider _tokenProvider; + private HubConnection? _instance; + private string _cachedConfigFor = string.Empty; + private HubConnectionConfig? _cachedConfig; + private bool _isDisposed = false; + + public HubFactory(ILogger logger, MareMediator mediator, + ServerConfigurationManager serverConfigurationManager, + TokenProvider tokenProvider, ILoggerProvider pluginLog) : base(logger, mediator) + { + _serverConfigurationManager = serverConfigurationManager; + _tokenProvider = tokenProvider; + _loggingProvider = pluginLog; + } + + public async Task DisposeHubAsync() + { + if (_instance == null || _isDisposed) return; + + Logger.LogDebug("Disposing current HubConnection"); + + _isDisposed = true; + + _instance.Closed -= HubOnClosed; + _instance.Reconnecting -= HubOnReconnecting; + _instance.Reconnected -= HubOnReconnected; + + await _instance.StopAsync().ConfigureAwait(false); + await _instance.DisposeAsync().ConfigureAwait(false); + + _instance = null; + + Logger.LogDebug("Current HubConnection disposed"); + } + + public async Task GetOrCreate(CancellationToken ct) + { + if (!_isDisposed && _instance != null) return _instance; + + _cachedConfig = await ResolveHubConfig().ConfigureAwait(false); + _cachedConfigFor = _serverConfigurationManager.CurrentApiUrl; + + return BuildHubConnection(_cachedConfig, ct); + } + + private async Task ResolveHubConfig() + { + var stapledWellKnown = _tokenProvider.GetStapledWellKnown(_serverConfigurationManager.CurrentApiUrl); + + var apiUrl = new Uri(_serverConfigurationManager.CurrentApiUrl); + + HubConnectionConfig defaultConfig; + + if (_cachedConfig != null && _serverConfigurationManager.CurrentApiUrl.Equals(_cachedConfigFor, StringComparison.Ordinal)) + { + defaultConfig = _cachedConfig; + } + else + { + defaultConfig = new HubConnectionConfig + { + HubUrl = _serverConfigurationManager.CurrentApiUrl.TrimEnd('/') + IMareHub.Path, + Transports = [] + }; + } + + string jsonResponse; + + if (stapledWellKnown != null) + { + jsonResponse = stapledWellKnown; + Logger.LogTrace("Using stapled hub config for {url}", _serverConfigurationManager.CurrentApiUrl); + } + else + { + try + { + var httpScheme = apiUrl.Scheme.ToLowerInvariant() switch + { + "ws" => "http", + "wss" => "https", + _ => apiUrl.Scheme + }; + + var wellKnownUrl = $"{httpScheme}://{apiUrl.Host}/.well-known/Snowcloak/client"; + Logger.LogTrace("Fetching hub config for {uri} via {wk}", _serverConfigurationManager.CurrentApiUrl, wellKnownUrl); + + using var httpClient = new HttpClient( + new HttpClientHandler + { + AllowAutoRedirect = true, + MaxAutomaticRedirections = 5 + } + ); + + var ver = Assembly.GetExecutingAssembly().GetName().Version; + httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); + + var response = await httpClient.GetAsync(wellKnownUrl).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + return defaultConfig; + + var contentType = response.Content.Headers.ContentType?.MediaType; + + if (contentType == null || !contentType.Equals("application/json", StringComparison.Ordinal)) + return defaultConfig; + + jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } + catch (HttpRequestException ex) + { + Logger.LogWarning(ex, "HTTP request failed for .well-known"); + return defaultConfig; + } + } + + try + { + var config = JsonSerializer.Deserialize(jsonResponse); + + if (config == null) + return defaultConfig; + + if (string.IsNullOrEmpty(config.ApiUrl)) + config.ApiUrl = defaultConfig.ApiUrl; + + if (string.IsNullOrEmpty(config.HubUrl)) + config.HubUrl = defaultConfig.HubUrl; + + config.Transports ??= defaultConfig.Transports ?? []; + + return config; + } + catch (JsonException ex) + { + Logger.LogWarning(ex, "Invalid JSON in .well-known response"); + return defaultConfig; + } + } + + private HubConnection BuildHubConnection(HubConnectionConfig hubConfig, CancellationToken ct) + { + Logger.LogDebug("Building new HubConnection"); + + _instance = new HubConnectionBuilder() + .WithUrl(hubConfig.HubUrl, options => + { + var transports = hubConfig.TransportType; + options.AccessTokenProvider = () => _tokenProvider.GetOrUpdateToken(ct); + options.SkipNegotiation = hubConfig.SkipNegotiation && (transports == HttpTransportType.WebSockets); + options.Transports = transports; + }) + .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); + }) + .WithAutomaticReconnect(new ForeverRetryPolicy(Mediator)) + .ConfigureLogging(a => + { + a.ClearProviders().AddProvider(_loggingProvider); + a.SetMinimumLevel(LogLevel.Information); + }) + .Build(); + + _instance.Closed += HubOnClosed; + _instance.Reconnecting += HubOnReconnecting; + _instance.Reconnected += HubOnReconnected; + + _isDisposed = false; + + return _instance; + } + + private Task HubOnClosed(Exception? arg) + { + Mediator.Publish(new HubClosedMessage(arg)); + return Task.CompletedTask; + } + + private Task HubOnReconnected(string? arg) + { + Mediator.Publish(new HubReconnectedMessage(arg)); + return Task.CompletedTask; + } + + private Task HubOnReconnecting(Exception? arg) + { + Mediator.Publish(new HubReconnectingMessage(arg)); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs b/MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs new file mode 100644 index 0000000..78ea0cb --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/JwtIdentifier.cs @@ -0,0 +1,9 @@ +namespace MareSynchronos.WebAPI.SignalR; + +public record JwtIdentifier(string ApiUrl, string CharaHash, string SecretKey) +{ + public override string ToString() + { + return "{JwtIdentifier; Url: " + ApiUrl + ", Chara: " + CharaHash + ", HasSecretKey: " + !string.IsNullOrEmpty(SecretKey) + "}"; + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/MareAuthFailureException.cs b/MareSynchronos/WebAPI/SignalR/MareAuthFailureException.cs new file mode 100644 index 0000000..10620e8 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/MareAuthFailureException.cs @@ -0,0 +1,11 @@ +namespace MareSynchronos.WebAPI.SignalR; + +public class MareAuthFailureException : Exception +{ + public MareAuthFailureException(string reason) + { + Reason = reason; + } + + public string Reason { get; } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/TokenProvider.cs b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs new file mode 100644 index 0000000..4867239 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs @@ -0,0 +1,183 @@ +using MareSynchronos.API.Routes; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using MareSynchronos.Services.ServerConfiguration; +using MareSynchronos.Utils; +using MareSynchronos.API.Dto; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Reflection; + +namespace MareSynchronos.WebAPI.SignalR; + +public sealed class TokenProvider : IDisposable, IMediatorSubscriber +{ + private readonly DalamudUtilService _dalamudUtil; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly ServerConfigurationManager _serverManager; + private readonly ConcurrentDictionary _tokenCache = new(); + private readonly ConcurrentDictionary _wellKnownCache = new(StringComparer.Ordinal); + + public TokenProvider(ILogger logger, ServerConfigurationManager serverManager, + DalamudUtilService dalamudUtil, MareMediator mareMediator) + { + _logger = logger; + _serverManager = serverManager; + _dalamudUtil = dalamudUtil; + _httpClient = new( + new HttpClientHandler + { + AllowAutoRedirect = true, + MaxAutomaticRedirections = 5 + } + ); + var ver = Assembly.GetExecutingAssembly().GetName().Version; + Mediator = mareMediator; + Mediator.Subscribe(this, (_) => + { + _lastJwtIdentifier = null; + _tokenCache.Clear(); + _wellKnownCache.Clear(); + }); + Mediator.Subscribe(this, (_) => + { + _lastJwtIdentifier = null; + _tokenCache.Clear(); + _wellKnownCache.Clear(); + }); + _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); + } + + public MareMediator Mediator { get; } + + private JwtIdentifier? _lastJwtIdentifier; + + public void Dispose() + { + Mediator.UnsubscribeAll(this); + _httpClient.Dispose(); + } + + public async Task GetNewToken(JwtIdentifier identifier, CancellationToken token) + { + Uri tokenUri; + HttpResponseMessage result; + + try + { + _logger.LogDebug("GetNewToken: Requesting"); + + tokenUri = MareAuth.AuthV2FullPath(new Uri(_serverManager.CurrentApiUrl + .Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase) + .Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase))); + var secretKey = _serverManager.GetSecretKey(out _)!; + var auth = secretKey.GetHash256(); + result = await _httpClient.PostAsync(tokenUri, new FormUrlEncodedContent([ + new("auth", auth), + new("charaIdent", await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false)), + ]), token).ConfigureAwait(false); + + if (!result.IsSuccessStatusCode) + { + Mediator.Publish(new NotificationMessage("Error refreshing token", "Your authentication token could not be renewed. Try reconnecting manually.", NotificationType.Error)); + Mediator.Publish(new DisconnectedMessage()); + var textResponse = await result.Content.ReadAsStringAsync(token).ConfigureAwait(false) ?? string.Empty; + throw new MareAuthFailureException(textResponse); + } + + var response = await result.Content.ReadFromJsonAsync(token).ConfigureAwait(false) ?? new(); + _tokenCache[identifier] = response.Token; + _wellKnownCache[_serverManager.CurrentApiUrl] = response.WellKnown; + return response.Token; + } + catch (HttpRequestException ex) + { + _tokenCache.TryRemove(identifier, out _); + _wellKnownCache.TryRemove(_serverManager.CurrentApiUrl, out _); + + _logger.LogError(ex, "GetNewToken: Failure to get token"); + + if (ex.StatusCode == HttpStatusCode.Unauthorized) + { + Mediator.Publish(new NotificationMessage("Error refreshing token", "Your authentication token could not be renewed. Try reconnecting manually.", NotificationType.Error)); + Mediator.Publish(new DisconnectedMessage()); + throw new MareAuthFailureException(ex.Message); + } + + throw; + } + } + + private async Task GetIdentifier() + { + JwtIdentifier jwtIdentifier; + try + { + var playerIdentifier = await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false); + + if (string.IsNullOrEmpty(playerIdentifier)) + { + _logger.LogTrace("GetIdentifier: PlayerIdentifier was null, returning last identifier {identifier}", _lastJwtIdentifier); + return _lastJwtIdentifier; + } + + jwtIdentifier = new(_serverManager.CurrentApiUrl, + playerIdentifier, + _serverManager.GetSecretKey(out _)!); + _lastJwtIdentifier = jwtIdentifier; + } + catch (Exception ex) + { + if (_lastJwtIdentifier == null) + { + _logger.LogError("GetIdentifier: No last identifier found, aborting"); + return null; + } + + _logger.LogWarning(ex, "GetIdentifier: Could not get JwtIdentifier for some reason or another, reusing last identifier {identifier}", _lastJwtIdentifier); + jwtIdentifier = _lastJwtIdentifier; + } + + _logger.LogDebug("GetIdentifier: Using identifier {identifier}", jwtIdentifier); + return jwtIdentifier; + } + + public async Task GetToken() + { + JwtIdentifier? jwtIdentifier = await GetIdentifier().ConfigureAwait(false); + if (jwtIdentifier == null) return null; + + if (_tokenCache.TryGetValue(jwtIdentifier, out var token)) + { + return token; + } + + throw new InvalidOperationException("No token present"); + } + + public async Task GetOrUpdateToken(CancellationToken ct) + { + JwtIdentifier? jwtIdentifier = await GetIdentifier().ConfigureAwait(false); + if (jwtIdentifier == null) return null; + + if (_tokenCache.TryGetValue(jwtIdentifier, out var token)) + return token; + + _logger.LogTrace("GetOrUpdate: Getting new token"); + return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false); + } + + public string? GetStapledWellKnown(string apiUrl) + { + _wellKnownCache.TryGetValue(apiUrl, out var wellKnown); + // Treat an empty string as null -- it won't decode as JSON anyway + if (string.IsNullOrEmpty(wellKnown)) + return null; + return wellKnown; + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/Utils/ForeverRetryPolicy.cs b/MareSynchronos/WebAPI/SignalR/Utils/ForeverRetryPolicy.cs new file mode 100644 index 0000000..835b048 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/Utils/ForeverRetryPolicy.cs @@ -0,0 +1,39 @@ +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services.Mediator; +using Microsoft.AspNetCore.SignalR.Client; + +namespace MareSynchronos.WebAPI.SignalR.Utils; + +public class ForeverRetryPolicy : IRetryPolicy +{ + private readonly MareMediator _mediator; + private bool _sentDisconnected = false; + + public ForeverRetryPolicy(MareMediator mediator) + { + _mediator = mediator; + } + + public TimeSpan? NextRetryDelay(RetryContext retryContext) + { + TimeSpan timeToWait = TimeSpan.FromSeconds(new Random().Next(10, 20)); + if (retryContext.PreviousRetryCount == 0) + { + _sentDisconnected = false; + timeToWait = TimeSpan.FromSeconds(3); + } + else if (retryContext.PreviousRetryCount == 1) timeToWait = TimeSpan.FromSeconds(5); + else if (retryContext.PreviousRetryCount == 2) timeToWait = TimeSpan.FromSeconds(10); + else + { + if (!_sentDisconnected) + { + _mediator.Publish(new NotificationMessage("Connection lost", "Connection lost to server", NotificationType.Warning, TimeSpan.FromSeconds(10))); + _mediator.Publish(new DisconnectedMessage()); + } + _sentDisconnected = true; + } + + return timeToWait; + } +} \ No newline at end of file diff --git a/MareSynchronos/WebAPI/SignalR/Utils/ServerState.cs b/MareSynchronos/WebAPI/SignalR/Utils/ServerState.cs new file mode 100644 index 0000000..ca34fe3 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/Utils/ServerState.cs @@ -0,0 +1,16 @@ +namespace MareSynchronos.WebAPI.SignalR.Utils; + +public enum ServerState +{ + Offline, + Connecting, + Reconnecting, + Disconnecting, + Disconnected, + Connected, + Unauthorized, + VersionMisMatch, + RateLimited, + NoSecretKey, + MultiChara, +} \ No newline at end of file diff --git a/MareSynchronos/packages.lock.json b/MareSynchronos/packages.lock.json new file mode 100644 index 0000000..94e0cec --- /dev/null +++ b/MareSynchronos/packages.lock.json @@ -0,0 +1,533 @@ +{ + "version": 1, + "dependencies": { + "net9.0-windows7.0": { + "Chaos.NaCl.Standard": { + "type": "Direct", + "requested": "[1.0.0, )", + "resolved": "1.0.0", + "contentHash": "8ajPyzu49LSIdPgeg56eDdKu1j8FZJngKgTn9rXHV3GNVly49XFvPAwoWuUT8xPze3OjVUOWQZaO82HWrYFFEw==" + }, + "DalamudPackager": { + "type": "Direct", + "requested": "[13.0.0, )", + "resolved": "13.0.0", + "contentHash": "Mb3cUDSK/vDPQ8gQIeuCw03EMYrej1B4J44a1AvIJ9C759p9XeqdU9Hg4WgOmlnlPe0G7ILTD32PKSUpkQNa8w==" + }, + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.25, )", + "resolved": "1.2.25", + "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + }, + "Downloader": { + "type": "Direct", + "requested": "[3.3.4, )", + "resolved": "3.3.4", + "contentHash": "/M/c80e1L0WW1XrLSSiQhgFxk8rrfbpWiWDn2CeBg1tPD393Neo+v184yG/ThyhE9rrNp36yCrugiCmEbRf+VQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.1" + } + }, + "Glamourer.Api": { + "type": "Direct", + "requested": "[2.6.0, )", + "resolved": "2.6.0", + "contentHash": "zysCZgNBRm3k3qvibyw/31MmEckX0Uh0ZsT+Sax3ZHnYIRELr9Qhbz3cjJz7u0RHGIrNJiRpktu/LxgHEqDItw==" + }, + "K4os.Compression.LZ4.Legacy": { + "type": "Direct", + "requested": "[1.3.8, )", + "resolved": "1.3.8", + "contentHash": "+82CK5zXbGjMnVLm2JJpsSAz3+4CRcMmxUDOmehUh1bDjrmpnc5VHxB14hiV1lXtqe53lNI+O76BQvgWhgI66g==", + "dependencies": { + "K4os.Compression.LZ4": "1.3.8" + } + }, + "K4os.Compression.LZ4.Streams": { + "type": "Direct", + "requested": "[1.3.8, )", + "resolved": "1.3.8", + "contentHash": "P15qr8dZAeo9GvYbUIPEYFQ0MEJ0i5iqr37wsYeRC3la2uCldOoeCa6to0CZ1taiwxIV+Mk8NGuZi+4iWivK9w==", + "dependencies": { + "K4os.Compression.LZ4": "1.3.8", + "K4os.Hash.xxHash": "1.0.8", + "System.IO.Pipelines": "6.0.3" + } + }, + "Meziantou.Analyzer": { + "type": "Direct", + "requested": "[2.0.212, )", + "resolved": "2.0.212", + "contentHash": "U91ktjjTRTccUs3Lk+hrLD9vW+2+lhnsOf4G1GpRSJi1pLn3uK5CU6wGP9Bmz1KlJs6Oz1GGoMhxQBoqQsmAuQ==" + }, + "Microsoft.AspNetCore.SignalR.Client": { + "type": "Direct", + "requested": "[9.0.8, )", + "resolved": "9.0.8", + "contentHash": "cO+TZaWdhMn2cIYfPH9oFZaisJrx7X6SBAYdmGektPUAW2BYtMbH4HyLOnJ5CYo42zP9WgqhWHKqmoDm7+Ol5w==", + "dependencies": { + "Microsoft.AspNetCore.Http.Connections.Client": "9.0.8", + "Microsoft.AspNetCore.SignalR.Client.Core": "9.0.8" + } + }, + "Microsoft.AspNetCore.SignalR.Protocols.MessagePack": { + "type": "Direct", + "requested": "[9.0.8, )", + "resolved": "9.0.8", + "contentHash": "e6SC/Tp+SZKeEVYdu8blz9q4MkFW08D56IkQv9V3perF3a7v+GgGZ0DAY/HRS9zBuhFrqpXhJvxeHMw3PJLcOg==", + "dependencies": { + "MessagePack": "2.5.187", + "Microsoft.AspNetCore.SignalR.Common": "9.0.8" + } + }, + "Microsoft.Extensions.Hosting": { + "type": "Direct", + "requested": "[9.0.8, )", + "resolved": "9.0.8", + "contentHash": "O2VlzORrBbS2it203k5FOHrudDdmdrJovA73P/shdRGeLzvet4e4yXhGx52V2PNjYBQ0IO5M4xiNcL+6xIX6Bg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", + "Microsoft.Extensions.Configuration.Binder": "9.0.8", + "Microsoft.Extensions.Configuration.CommandLine": "9.0.8", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "9.0.8", + "Microsoft.Extensions.Configuration.FileExtensions": "9.0.8", + "Microsoft.Extensions.Configuration.Json": "9.0.8", + "Microsoft.Extensions.Configuration.UserSecrets": "9.0.8", + "Microsoft.Extensions.DependencyInjection": "9.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Diagnostics": "9.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8", + "Microsoft.Extensions.FileProviders.Physical": "9.0.8", + "Microsoft.Extensions.Hosting.Abstractions": "9.0.8", + "Microsoft.Extensions.Logging": "9.0.8", + "Microsoft.Extensions.Logging.Abstractions": "9.0.8", + "Microsoft.Extensions.Logging.Configuration": "9.0.8", + "Microsoft.Extensions.Logging.Console": "9.0.8", + "Microsoft.Extensions.Logging.Debug": "9.0.8", + "Microsoft.Extensions.Logging.EventLog": "9.0.8", + "Microsoft.Extensions.Logging.EventSource": "9.0.8", + "Microsoft.Extensions.Options": "9.0.8" + } + }, + "Penumbra.Api": { + "type": "Direct", + "requested": "[5.12.0, )", + "resolved": "5.12.0", + "contentHash": "XGWviAZgokj2djpH50FWgM24jOTpKUuDHvd0HwrzBRY6BEMmpb3HfGIl8+BDE/DqbpH63u6aO2TvzUV6BmXT5w==" + }, + "SonarAnalyzer.CSharp": { + "type": "Direct", + "requested": "[10.15.0.120848, )", + "resolved": "10.15.0.120848", + "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ==" + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Direct", + "requested": "[8.14.0, )", + "resolved": "8.14.0", + "contentHash": "EYGgN/S+HK7S6F3GaaPLFAfK0UzMrkXFyWCvXpQWFYmZln3dqtbyIO7VuTM/iIIPMzkelg8ZLlBPvMhxj6nOAA==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.14.0", + "Microsoft.IdentityModel.Tokens": "8.14.0" + } + }, + "K4os.Compression.LZ4": { + "type": "Transitive", + "resolved": "1.3.8", + "contentHash": "LhwlPa7c1zs1OV2XadMtAWdImjLIsqFJPoRcIWAadSRn0Ri1DepK65UbWLPmt4riLqx2d40xjXRk0ogpqNtK7g==" + }, + "K4os.Hash.xxHash": { + "type": "Transitive", + "resolved": "1.0.8", + "contentHash": "Wp2F7BamQ2Q/7Hk834nV9vRQapgcr8kgv9Jvfm8J3D0IhDqZMMl+a2yxUq5ltJitvXvQfB8W6K4F4fCbw/P6YQ==" + }, + "MessagePack": { + "type": "Transitive", + "resolved": "2.5.187", + "contentHash": "uW4j8m4Nc+2Mk5n6arOChavJ9bLjkis0qWASOj2h2OwmfINuzYv+mjCHUymrYhmyyKTu3N+ObtTXAY4uQ7jIhg==", + "dependencies": { + "MessagePack.Annotations": "2.5.187", + "Microsoft.NET.StringTools": "17.6.3" + } + }, + "MessagePack.Annotations": { + "type": "Transitive", + "resolved": "2.5.187", + "contentHash": "/IvvMMS8opvlHjEJ/fR2Cal4Co726Kj77Z8KiohFhuHfLHHmb9uUxW5+tSCL4ToKFfkQlrS3HD638mRq83ySqA==" + }, + "Microsoft.AspNetCore.Connections.Abstractions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "mONfcKx7I4h6Rg+3b20bRyuy/GWz2yLsCNzKKqh1X4OfxnI7l0rdSxBwO203ebZFhjrdXnqMl7Op0N1FQ1Q5DQ==", + "dependencies": { + "Microsoft.Extensions.Features": "9.0.8" + } + }, + "Microsoft.AspNetCore.Http.Connections.Client": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "Ob2n+H3358kvubgXu9hY95MZB6X91PUGJvtWaHGEX7eZ+9bYdUCYs57ukJiIziH+aD9yO9e36bgKIT1WJEtfmA==", + "dependencies": { + "Microsoft.AspNetCore.Http.Connections.Common": "9.0.8", + "Microsoft.Extensions.Logging.Abstractions": "9.0.8", + "Microsoft.Extensions.Options": "9.0.8", + "System.Net.ServerSentEvents": "9.0.8" + } + }, + "Microsoft.AspNetCore.Http.Connections.Common": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "150BRlecnjL+6C+yw/bDP49+ONh7BmaJZTRik6KtbaS+cWnEDVXnhE5PTKlFqCYBD5T8wdjKoF5+lzKHJUK47A==", + "dependencies": { + "Microsoft.AspNetCore.Connections.Abstractions": "9.0.8" + } + }, + "Microsoft.AspNetCore.SignalR.Client.Core": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "EZ4KaPVQ9rDxZYWQ1sYiPfXEbomhKwp5Fn/0q1XtOgTilV/nN2lgA06KTofVJSeVVRwYdlZggflcQNcKCG0xcg==", + "dependencies": { + "Microsoft.AspNetCore.SignalR.Common": "9.0.8", + "Microsoft.AspNetCore.SignalR.Protocols.Json": "9.0.8", + "Microsoft.Extensions.DependencyInjection": "9.0.8", + "Microsoft.Extensions.Logging": "9.0.8", + "System.Threading.Channels": "9.0.8" + } + }, + "Microsoft.AspNetCore.SignalR.Common": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "oNOEDf2UGLU63Qi7LB8OJdfG1CGybVO34bhotpkvAQUJ5zH8Ewf7EvqeHlUgg6cVyrdC+vewOFxTysw212FTyw==", + "dependencies": { + "Microsoft.AspNetCore.Connections.Abstractions": "9.0.8", + "Microsoft.Extensions.Options": "9.0.8" + } + }, + "Microsoft.AspNetCore.SignalR.Protocols.Json": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "9LtBkzS2iYOSiUx1NDI91abM5xxD5MUYtdlvwCtMMr6YdsMzHvDUrgPK2N3hpYE94vmj0srt423Kwd1aOqmGPg==", + "dependencies": { + "Microsoft.AspNetCore.SignalR.Common": "9.0.8" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "6m+8Xgmf8UWL0p/oGqBM+0KbHE5/ePXbV1hKXgC59zEv0aa0DW5oiiyxDbK5kH5j4gIvyD5uWL0+HadKBJngvQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", + "Microsoft.Extensions.Primitives": "9.0.8" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "yNou2KM35RvzOh4vUFtl2l33rWPvOCoba+nzEDJ+BgD8aOL/jew4WPCibQvntRfOJ2pJU8ARygSMD+pdjvDHuA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.8" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "0vK9DnYrYChdiH3yRZWkkp4x4LbrfkWEdBc5HOsQ8t/0CLOWKXKkkhOE8A1shlex0hGydbGrhObeypxz/QTm+w==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "vB6eDQ5prED5jHBqmSDNYzlCXsTSylYY7co9c7guhnz0zhx+jZ8BTHgO7y/Wl1dV2jAO15mKNWuyHRIRtWwGQg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "9qileEYXDodlPN9DfPd5sHSfU2nSrI1r5BHVqLaLyb/7mPi335cy4ar/0ix4tXb2Aer/Pu4e5/zdwxt7lrtSyQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "2jgx58Jpk3oKT7KRn8x/cFf3QDTjQP+KUbyBnynAcB2iBx1Eq9EdNMCu0QEbYuaZOaQru/Kwdffary+hn58Wwg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8", + "Microsoft.Extensions.FileProviders.Physical": "9.0.8", + "Microsoft.Extensions.Primitives": "9.0.8" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "vjxzcnL7ul322+kpvELisXaZl8/5MYs6JfI9DZLQWsao1nA/4FL48yPwDK986hbJTWc64JxOOaMym0SQ/dy32w==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", + "Microsoft.Extensions.Configuration.FileExtensions": "9.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "UgH18nQkuMJgxjn1539I83N6LhnKQlLhQm3ppe+PGsFpYsC6eGpF/1KvDRm/bmqsrg0NXhurrv4k2r0e8vWX/Q==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", + "Microsoft.Extensions.Configuration.Json": "9.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8", + "Microsoft.Extensions.FileProviders.Physical": "9.0.8" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "JJjI2Fa+QtZcUyuNjbKn04OjIUX5IgFGFu/Xc+qvzh1rXdZHLcnqqVXhR4093bGirTwacRlHiVg1XYI9xum6QQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "xY3lTjj4+ZYmiKIkyWitddrp1uL5uYiweQjqo4BKBw01ZC4HhcfgLghDpPZcUlppgWAFqFy9SgkiYWOMx365pw==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "BKkLCFXzJvNmdngeYBf72VXoZqTJSb1orvjdzDLaGobicoGFBPW8ug2ru1nnEewMEwJzMgnsjHQY8EaKWmVhKg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.8", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.8", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.8" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "UDY7blv4DCyIJ/8CkNrQKLaAZFypXQavRZ2DWf/2zi1mxYYKKw2t8AOCBWxNntyPZHPGhtEmL3snFM98ADZqTw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Options": "9.0.8" + } + }, + "Microsoft.Extensions.Features": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "oyPrbpRFa0uWik3PMwpK1mbAr+inZTEkaBsnMjHyT74YN0ot6knA7OnyFLg+oM4MwW5PZIS4HHW9efy0+gj+oQ==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "4zZbQ4w+hCMm9J+z5NOj3giIPT2MhZxx05HX/MGuAmDBbjOuXlYIIRN+t4V6OLxy5nXZIcXO+dQMB/OWubuDkw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.8" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "FlOe2i7UUIfY0l0ChaIYtlXjdWWutR4DMRKZaGD6z4G1uVTteFkbBfxUIoi1uGmrZQxXe/yv/cfwiT0tK2xyXA==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8", + "Microsoft.Extensions.FileSystemGlobbing": "9.0.8", + "Microsoft.Extensions.Primitives": "9.0.8" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "96Ub5LmwYfIGVoXkbe4kjs+ivK6fLBTwKJAOMfUNV0R+AkZRItlgROFqXEWMUlXBTPM1/kKu26Ueu5As6RDzJA==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "WNrad20tySNCPe9aJUK7Wfwh+RiyLF+id02FKW8Qfc+HAzNQHazcqMXAbwG/kmbS89uvan/nKK1MufkRahjrJA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.8", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8", + "Microsoft.Extensions.Logging.Abstractions": "9.0.8" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "Z/7ze+0iheT7FJeZPqJKARYvyC2bmwu3whbm/48BJjdlGVvgDguoCqJIkI/67NkroTYobd5geai1WheNQvWrgA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.8", + "Microsoft.Extensions.Logging.Abstractions": "9.0.8", + "Microsoft.Extensions.Options": "9.0.8" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "pYnAffJL7ARD/HCnnPvnFKSIHnTSmWz84WIlT9tPeQ4lHNiu0Az7N/8itihWvcF8sT+VVD5lq8V+ckMzu4SbOw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "Us4evDN3lbp1beVgrpxkSXKrbntVGAK+YbSo9P9driiU9PK05+ShhgesJ3aj7SuDfr3mqqcEgrMJ87Vu8t5dhw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.8", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", + "Microsoft.Extensions.Configuration.Binder": "9.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Logging": "9.0.8", + "Microsoft.Extensions.Logging.Abstractions": "9.0.8", + "Microsoft.Extensions.Options": "9.0.8", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.8" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "mPp9xB9MjiPuodh9z/+6zEGNj2kSVeXQtdbIBHlhUYqxX22gzJkx0ycPY42q4/OT/SzFV/TJ989Pa3sA/8ZBeA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Logging": "9.0.8", + "Microsoft.Extensions.Logging.Abstractions": "9.0.8", + "Microsoft.Extensions.Logging.Configuration": "9.0.8", + "Microsoft.Extensions.Options": "9.0.8" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "OwHQFVITsONEoizShc1yNYTUvMq0kT9j/LhwAKMsA7OZqtrBXuqjosbSvzkJZ9o+KWAozDh5Y1Vtpe5p/8/1qA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Logging": "9.0.8", + "Microsoft.Extensions.Logging.Abstractions": "9.0.8" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "/gMwlll21UJcaXlitUqd+rs9jH36EJz5BpFVPshyOqz5u0qyV1pFnTWm5vhyx+g6gwVYENSLgpazR1urNv83xw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Logging": "9.0.8", + "Microsoft.Extensions.Logging.Abstractions": "9.0.8", + "Microsoft.Extensions.Options": "9.0.8", + "System.Diagnostics.EventLog": "9.0.8" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "aGMFc/1P+315d07iyxSe6lEoZ0JjOPJ+Mfv9rrV2PvR2DFu1/pSi/SItHw1iChJOZgslNKJE97g1a9nLX3qQYA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Logging": "9.0.8", + "Microsoft.Extensions.Logging.Abstractions": "9.0.8", + "Microsoft.Extensions.Options": "9.0.8", + "Microsoft.Extensions.Primitives": "9.0.8" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "OmTaQ0v4gxGQkehpwWIqPoEiwsPuG/u4HUsbOFoWGx4DKET2AXzopnFe/fE608FIhzc/kcg2p8JdyMRCCUzitQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Primitives": "9.0.8" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "eW2s6n06x0w6w4nsX+SvpgsFYkl+Y0CttYAt6DKUXeqprX+hzNqjSfOh637fwNJBg7wRBrOIRHe49gKiTgJxzQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.8", + "Microsoft.Extensions.Configuration.Binder": "9.0.8", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8", + "Microsoft.Extensions.Options": "9.0.8", + "Microsoft.Extensions.Primitives": "9.0.8" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "tizSIOEsIgSNSSh+hKeUVPK7xmTIjR8s+mJWOu1KXV3htvNQiPMFRMO17OdI1y/4ZApdBVk49u/08QGC9yvLug==" + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "iwbCpSjD3ehfTwBhtSNEtKPK0ICun6ov7Ibx6ISNA9bfwIyzI2Siwyi9eJFCJBwxowK9xcA1mj+jBWiigeqgcQ==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "4jOpiA4THdtpLyMdAb24dtj7+6GmvhOhxf5XHLYWmPKF8ApEnApal1UnJsKO4HxUWRXDA6C4WQVfYyqsRhpNpQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.14.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "eqqnemdW38CKZEHS6diA50BV94QICozDZEvSrsvN3SJXUFwVB9gy+/oz76gldP7nZliA16IglXjXTCTdmU/Ejg==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.14.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.14.0", + "contentHash": "lKIZiBiGd36k02TCdMHp1KlNWisyIvQxcYJvIkz7P4gSQ9zi8dgh6S5Grj8NNG7HWYIPfQymGyoZ6JB5d1Lo1g==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.14.0" + } + }, + "Microsoft.NET.StringTools": { + "type": "Transitive", + "resolved": "17.6.3", + "contentHash": "N0ZIanl1QCgvUumEL1laasU0a7sOE5ZwLZVTn0pAePnfhq8P7SvTjF8Axq+CnavuQkmdQpGNXQ1efZtu5kDFbA==" + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "gebRF3JLLJ76jz1CQpvwezNapZUjFq20JQsaGHzBH0DzlkHBLpdhwkOei9usiOkIGMwU/L0ALWpNe1JE+5/itw==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "6.0.3", + "contentHash": "ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw==" + }, + "System.Net.ServerSentEvents": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "wrpra4YvKXL7VdsQMKPcPxyA8pXK22LcxaKGA8oEndgjLZ1ZSdKXTxEA2cPvvNpMEUBwZlgJ6oZYQ8aJcpapPg==" + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "9.0.8", + "contentHash": "kpvkzWJoHR9os3/4LL5feaTTLD92+XzTqPyYLU2tw2BoJ4MrWCfkjGXtL7MsdpV/20e1+SamCbrPj2L9ptwgBA==" + }, + "maresynchronos.api": { + "type": "Project", + "dependencies": { + "MessagePack.Annotations": "[2.5.129, )" + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra.Api/.editorconfig b/Penumbra.Api/.editorconfig new file mode 100644 index 0000000..e3be45d --- /dev/null +++ b/Penumbra.Api/.editorconfig @@ -0,0 +1,3625 @@ +# Standard properties +charset = utf-8 +end_of_line = lf +insert_final_newline = true +csharp_indent_labels = one_less_than_current +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_prefer_top_level_statements = true:silent + +[*] +# Microsoft .NET properties +csharp_indent_braces=false +csharp_indent_switch_labels=true +csharp_new_line_before_catch=true +csharp_new_line_before_else=true +csharp_new_line_before_finally=true +csharp_new_line_before_members_in_object_initializers=true +csharp_new_line_before_open_brace=all +csharp_new_line_between_query_expression_clauses=true +csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_preserve_single_line_blocks=true +csharp_space_after_cast=false +csharp_space_after_colon_in_inheritance_clause=true +csharp_space_after_comma=true +csharp_space_after_dot=false +csharp_space_after_keywords_in_control_flow_statements=true +csharp_space_after_semicolon_in_for_statement=true +csharp_space_around_binary_operators=before_and_after +csharp_space_before_colon_in_inheritance_clause=true +csharp_space_before_comma=false +csharp_space_before_dot=false +csharp_space_before_open_square_brackets=false +csharp_space_before_semicolon_in_for_statement=false +csharp_space_between_empty_square_brackets=false +csharp_space_between_method_call_empty_parameter_list_parentheses=false +csharp_space_between_method_call_name_and_opening_parenthesis=false +csharp_space_between_method_call_parameter_list_parentheses=false +csharp_space_between_method_declaration_empty_parameter_list_parentheses=false +csharp_space_between_method_declaration_name_and_open_parenthesis=false +csharp_space_between_method_declaration_parameter_list_parentheses=false +csharp_space_between_parentheses=false +csharp_space_between_square_brackets=false +csharp_style_namespace_declarations= file_scoped:suggestion +csharp_style_var_elsewhere=true:suggestion +csharp_style_var_for_built_in_types=true:suggestion +csharp_style_var_when_type_is_apparent=true:suggestion +csharp_using_directive_placement= outside_namespace:silent +dotnet_diagnostic.bc40000.severity=warning +dotnet_diagnostic.bc400005.severity=warning +dotnet_diagnostic.bc40008.severity=warning +dotnet_diagnostic.bc40056.severity=warning +dotnet_diagnostic.bc42016.severity=warning +dotnet_diagnostic.bc42024.severity=warning +dotnet_diagnostic.bc42025.severity=warning +dotnet_diagnostic.bc42104.severity=warning +dotnet_diagnostic.bc42105.severity=warning +dotnet_diagnostic.bc42106.severity=warning +dotnet_diagnostic.bc42107.severity=warning +dotnet_diagnostic.bc42304.severity=warning +dotnet_diagnostic.bc42309.severity=warning +dotnet_diagnostic.bc42322.severity=warning +dotnet_diagnostic.bc42349.severity=warning +dotnet_diagnostic.bc42353.severity=warning +dotnet_diagnostic.bc42354.severity=warning +dotnet_diagnostic.bc42355.severity=warning +dotnet_diagnostic.bc42356.severity=warning +dotnet_diagnostic.bc42358.severity=warning +dotnet_diagnostic.bc42504.severity=warning +dotnet_diagnostic.bc42505.severity=warning +dotnet_diagnostic.cs0067.severity=warning +dotnet_diagnostic.cs0078.severity=warning +dotnet_diagnostic.cs0108.severity=warning +dotnet_diagnostic.cs0109.severity=warning +dotnet_diagnostic.cs0114.severity=warning +dotnet_diagnostic.cs0162.severity=warning +dotnet_diagnostic.cs0164.severity=warning +dotnet_diagnostic.cs0168.severity=warning +dotnet_diagnostic.cs0169.severity=warning +dotnet_diagnostic.cs0183.severity=warning +dotnet_diagnostic.cs0184.severity=warning +dotnet_diagnostic.cs0197.severity=warning +dotnet_diagnostic.cs0219.severity=warning +dotnet_diagnostic.cs0252.severity=warning +dotnet_diagnostic.cs0253.severity=warning +dotnet_diagnostic.cs0414.severity=warning +dotnet_diagnostic.cs0420.severity=warning +dotnet_diagnostic.cs0465.severity=warning +dotnet_diagnostic.cs0469.severity=warning +dotnet_diagnostic.cs0612.severity=warning +dotnet_diagnostic.cs0618.severity=warning +dotnet_diagnostic.cs0628.severity=warning +dotnet_diagnostic.cs0642.severity=warning +dotnet_diagnostic.cs0649.severity=warning +dotnet_diagnostic.cs0652.severity=warning +dotnet_diagnostic.cs0657.severity=warning +dotnet_diagnostic.cs0658.severity=warning +dotnet_diagnostic.cs0659.severity=warning +dotnet_diagnostic.cs0660.severity=warning +dotnet_diagnostic.cs0661.severity=warning +dotnet_diagnostic.cs0665.severity=warning +dotnet_diagnostic.cs0672.severity=warning +dotnet_diagnostic.cs0675.severity=warning +dotnet_diagnostic.cs0693.severity=warning +dotnet_diagnostic.cs1030.severity=warning +dotnet_diagnostic.cs1058.severity=warning +dotnet_diagnostic.cs1066.severity=warning +dotnet_diagnostic.cs1522.severity=warning +dotnet_diagnostic.cs1570.severity=warning +dotnet_diagnostic.cs1571.severity=warning +dotnet_diagnostic.cs1572.severity=warning +dotnet_diagnostic.cs1573.severity=warning +dotnet_diagnostic.cs1574.severity=warning +dotnet_diagnostic.cs1580.severity=warning +dotnet_diagnostic.cs1581.severity=warning +dotnet_diagnostic.cs1584.severity=warning +dotnet_diagnostic.cs1587.severity=warning +dotnet_diagnostic.cs1589.severity=warning +dotnet_diagnostic.cs1590.severity=warning +dotnet_diagnostic.cs1591.severity=warning +dotnet_diagnostic.cs1592.severity=warning +dotnet_diagnostic.cs1710.severity=warning +dotnet_diagnostic.cs1711.severity=warning +dotnet_diagnostic.cs1712.severity=warning +dotnet_diagnostic.cs1717.severity=warning +dotnet_diagnostic.cs1723.severity=warning +dotnet_diagnostic.cs1911.severity=warning +dotnet_diagnostic.cs1957.severity=warning +dotnet_diagnostic.cs1981.severity=warning +dotnet_diagnostic.cs1998.severity=warning +dotnet_diagnostic.cs4014.severity=warning +dotnet_diagnostic.cs7022.severity=warning +dotnet_diagnostic.cs7023.severity=warning +dotnet_diagnostic.cs7095.severity=warning +dotnet_diagnostic.cs8094.severity=warning +dotnet_diagnostic.cs8123.severity=warning +dotnet_diagnostic.cs8321.severity=warning +dotnet_diagnostic.cs8383.severity=warning +dotnet_diagnostic.cs8416.severity=warning +dotnet_diagnostic.cs8417.severity=warning +dotnet_diagnostic.cs8424.severity=warning +dotnet_diagnostic.cs8425.severity=warning +dotnet_diagnostic.cs8509.severity=warning +dotnet_diagnostic.cs8524.severity=warning +dotnet_diagnostic.cs8597.severity=warning +dotnet_diagnostic.cs8600.severity=warning +dotnet_diagnostic.cs8601.severity=warning +dotnet_diagnostic.cs8602.severity=warning +dotnet_diagnostic.cs8603.severity=warning +dotnet_diagnostic.cs8604.severity=warning +dotnet_diagnostic.cs8605.severity=warning +dotnet_diagnostic.cs8607.severity=warning +dotnet_diagnostic.cs8608.severity=warning +dotnet_diagnostic.cs8609.severity=warning +dotnet_diagnostic.cs8610.severity=warning +dotnet_diagnostic.cs8611.severity=warning +dotnet_diagnostic.cs8612.severity=warning +dotnet_diagnostic.cs8613.severity=warning +dotnet_diagnostic.cs8614.severity=warning +dotnet_diagnostic.cs8615.severity=warning +dotnet_diagnostic.cs8616.severity=warning +dotnet_diagnostic.cs8617.severity=warning +dotnet_diagnostic.cs8618.severity=warning +dotnet_diagnostic.cs8619.severity=warning +dotnet_diagnostic.cs8620.severity=warning +dotnet_diagnostic.cs8621.severity=warning +dotnet_diagnostic.cs8622.severity=warning +dotnet_diagnostic.cs8624.severity=warning +dotnet_diagnostic.cs8625.severity=warning +dotnet_diagnostic.cs8629.severity=warning +dotnet_diagnostic.cs8631.severity=warning +dotnet_diagnostic.cs8632.severity=none +dotnet_diagnostic.cs8633.severity=warning +dotnet_diagnostic.cs8634.severity=warning +dotnet_diagnostic.cs8643.severity=warning +dotnet_diagnostic.cs8644.severity=warning +dotnet_diagnostic.cs8645.severity=warning +dotnet_diagnostic.cs8655.severity=warning +dotnet_diagnostic.cs8656.severity=warning +dotnet_diagnostic.cs8667.severity=warning +dotnet_diagnostic.cs8669.severity=none +dotnet_diagnostic.cs8670.severity=warning +dotnet_diagnostic.cs8714.severity=warning +dotnet_diagnostic.cs8762.severity=warning +dotnet_diagnostic.cs8763.severity=warning +dotnet_diagnostic.cs8764.severity=warning +dotnet_diagnostic.cs8765.severity=warning +dotnet_diagnostic.cs8766.severity=warning +dotnet_diagnostic.cs8767.severity=warning +dotnet_diagnostic.cs8768.severity=warning +dotnet_diagnostic.cs8769.severity=warning +dotnet_diagnostic.cs8770.severity=warning +dotnet_diagnostic.cs8774.severity=warning +dotnet_diagnostic.cs8775.severity=warning +dotnet_diagnostic.cs8776.severity=warning +dotnet_diagnostic.cs8777.severity=warning +dotnet_diagnostic.cs8794.severity=warning +dotnet_diagnostic.cs8819.severity=warning +dotnet_diagnostic.cs8824.severity=warning +dotnet_diagnostic.cs8825.severity=warning +dotnet_diagnostic.cs8846.severity=warning +dotnet_diagnostic.cs8847.severity=warning +dotnet_diagnostic.cs8851.severity=warning +dotnet_diagnostic.cs8860.severity=warning +dotnet_diagnostic.cs8892.severity=warning +dotnet_diagnostic.cs8907.severity=warning +dotnet_diagnostic.cs8947.severity=warning +dotnet_diagnostic.cs8960.severity=warning +dotnet_diagnostic.cs8961.severity=warning +dotnet_diagnostic.cs8962.severity=warning +dotnet_diagnostic.cs8963.severity=warning +dotnet_diagnostic.cs8965.severity=warning +dotnet_diagnostic.cs8966.severity=warning +dotnet_diagnostic.cs8971.severity=warning +dotnet_diagnostic.wme006.severity=warning +dotnet_naming_rule.constants_rule.import_to_resharper=as_predefined +dotnet_naming_rule.constants_rule.severity = warning +dotnet_naming_rule.constants_rule.style = upper_camel_case_style +dotnet_naming_rule.constants_rule.symbols=constants_symbols +dotnet_naming_rule.event_rule.import_to_resharper=as_predefined +dotnet_naming_rule.event_rule.severity = warning +dotnet_naming_rule.event_rule.style = upper_camel_case_style +dotnet_naming_rule.event_rule.symbols=event_symbols +dotnet_naming_rule.interfaces_rule.import_to_resharper=as_predefined +dotnet_naming_rule.interfaces_rule.severity = warning +dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style +dotnet_naming_rule.interfaces_rule.symbols=interfaces_symbols +dotnet_naming_rule.locals_rule.import_to_resharper=as_predefined +dotnet_naming_rule.locals_rule.severity = warning +dotnet_naming_rule.locals_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.locals_rule.symbols=locals_symbols +dotnet_naming_rule.local_constants_rule.import_to_resharper=as_predefined +dotnet_naming_rule.local_constants_rule.severity = warning +dotnet_naming_rule.local_constants_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.local_constants_rule.symbols=local_constants_symbols +dotnet_naming_rule.local_functions_rule.import_to_resharper=as_predefined +dotnet_naming_rule.local_functions_rule.severity = warning +dotnet_naming_rule.local_functions_rule.style = upper_camel_case_style +dotnet_naming_rule.local_functions_rule.symbols=local_functions_symbols +dotnet_naming_rule.method_rule.import_to_resharper=as_predefined +dotnet_naming_rule.method_rule.severity = warning +dotnet_naming_rule.method_rule.style = upper_camel_case_style +dotnet_naming_rule.method_rule.symbols=method_symbols +dotnet_naming_rule.parameters_rule.import_to_resharper=as_predefined +dotnet_naming_rule.parameters_rule.severity = warning +dotnet_naming_rule.parameters_rule.style = lower_camel_case_style_1 +dotnet_naming_rule.parameters_rule.symbols=parameters_symbols +dotnet_naming_rule.private_constants_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_constants_rule.severity = warning +dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style +dotnet_naming_rule.private_constants_rule.symbols=private_constants_symbols +dotnet_naming_rule.private_instance_fields_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_instance_fields_rule.severity = warning +dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_instance_fields_rule.symbols=private_instance_fields_symbols +dotnet_naming_rule.private_static_fields_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_static_fields_rule.severity = warning +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style +dotnet_naming_rule.private_static_fields_rule.symbols=private_static_fields_symbols +dotnet_naming_rule.private_static_readonly_rule.import_to_resharper=as_predefined +dotnet_naming_rule.private_static_readonly_rule.severity = warning +dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_readonly_rule.symbols=private_static_readonly_symbols +dotnet_naming_rule.property_rule.import_to_resharper=as_predefined +dotnet_naming_rule.property_rule.severity = warning +dotnet_naming_rule.property_rule.style = upper_camel_case_style +dotnet_naming_rule.property_rule.symbols=property_symbols +dotnet_naming_rule.public_fields_rule.import_to_resharper=as_predefined +dotnet_naming_rule.public_fields_rule.severity = warning +dotnet_naming_rule.public_fields_rule.style = upper_camel_case_style +dotnet_naming_rule.public_fields_rule.symbols=public_fields_symbols +dotnet_naming_rule.static_readonly_rule.import_to_resharper=as_predefined +dotnet_naming_rule.static_readonly_rule.severity = warning +dotnet_naming_rule.static_readonly_rule.style = upper_camel_case_style +dotnet_naming_rule.static_readonly_rule.symbols=static_readonly_symbols +dotnet_naming_rule.types_and_namespaces_rule.import_to_resharper=as_predefined +dotnet_naming_rule.types_and_namespaces_rule.severity = warning +dotnet_naming_rule.types_and_namespaces_rule.style = upper_camel_case_style +dotnet_naming_rule.types_and_namespaces_rule.symbols=types_and_namespaces_symbols +dotnet_naming_rule.type_parameters_rule.import_to_resharper=as_predefined +dotnet_naming_rule.type_parameters_rule.severity = warning +dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style +dotnet_naming_rule.type_parameters_rule.symbols=type_parameters_symbols +dotnet_naming_style.i_upper_camel_case_style.capitalization=pascal_case +dotnet_naming_style.i_upper_camel_case_style.required_prefix=I +dotnet_naming_style.lower_camel_case_style.capitalization=camel_case +dotnet_naming_style.lower_camel_case_style.required_prefix=_ +dotnet_naming_style.lower_camel_case_style_1.capitalization=camel_case +dotnet_naming_style.t_upper_camel_case_style.capitalization=pascal_case +dotnet_naming_style.t_upper_camel_case_style.required_prefix=T +dotnet_naming_style.upper_camel_case_style.capitalization=pascal_case +dotnet_naming_symbols.constants_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.constants_symbols.applicable_kinds=field +dotnet_naming_symbols.constants_symbols.required_modifiers=const +dotnet_naming_symbols.event_symbols.applicable_accessibilities=* +dotnet_naming_symbols.event_symbols.applicable_kinds=event +dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities=* +dotnet_naming_symbols.interfaces_symbols.applicable_kinds=interface +dotnet_naming_symbols.locals_symbols.applicable_accessibilities=* +dotnet_naming_symbols.locals_symbols.applicable_kinds=local +dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities=* +dotnet_naming_symbols.local_constants_symbols.applicable_kinds=local +dotnet_naming_symbols.local_constants_symbols.required_modifiers=const +dotnet_naming_symbols.local_functions_symbols.applicable_accessibilities=* +dotnet_naming_symbols.local_functions_symbols.applicable_kinds=local_function +dotnet_naming_symbols.method_symbols.applicable_accessibilities=* +dotnet_naming_symbols.method_symbols.applicable_kinds=method +dotnet_naming_symbols.parameters_symbols.applicable_accessibilities=* +dotnet_naming_symbols.parameters_symbols.applicable_kinds=parameter +dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_constants_symbols.applicable_kinds=field +dotnet_naming_symbols.private_constants_symbols.required_modifiers=const +dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds=field +dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds=field +dotnet_naming_symbols.private_static_fields_symbols.required_modifiers=static +dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities=private +dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds=field +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers=static,readonly +dotnet_naming_symbols.property_symbols.applicable_accessibilities=* +dotnet_naming_symbols.property_symbols.applicable_kinds=property +dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_fields_symbols.applicable_kinds=field +dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities=public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.static_readonly_symbols.applicable_kinds=field +dotnet_naming_symbols.static_readonly_symbols.required_modifiers=static,readonly +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities=* +dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds=namespace,class,struct,enum,delegate +dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities=* +dotnet_naming_symbols.type_parameters_symbols.applicable_kinds=type_parameter +dotnet_separate_import_directive_groups=false +dotnet_sort_system_directives_first=true +dotnet_style_parentheses_in_arithmetic_binary_operators=never_if_unnecessary:suggestion +dotnet_style_parentheses_in_other_binary_operators=never_if_unnecessary:suggestion +dotnet_style_parentheses_in_relational_binary_operators=never_if_unnecessary:suggestion +dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion +dotnet_style_predefined_type_for_member_access=true:suggestion +dotnet_style_qualification_for_event=false:suggestion +dotnet_style_qualification_for_field=false:suggestion +dotnet_style_qualification_for_method=false:suggestion +dotnet_style_qualification_for_property=false:suggestion +dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion +file_header_template= + +# ReSharper properties +resharper_accessor_owner_body=expression_body +resharper_alignment_tab_fill_style=use_spaces +resharper_align_first_arg_by_paren=false +resharper_align_linq_query=false +resharper_align_multiline_argument=true +resharper_align_multiline_array_and_object_initializer=false +resharper_align_multiline_array_initializer=true +resharper_align_multiline_binary_expressions_chain=false +resharper_align_multiline_binary_patterns=false +resharper_align_multiline_ctor_init=true +resharper_align_multiline_expression_braces=false +resharper_align_multiline_implements_list=true +resharper_align_multiline_property_pattern=false +resharper_align_multiline_statement_conditions=true +resharper_align_multiline_switch_expression=false +resharper_align_multiline_type_argument=true +resharper_align_multiline_type_parameter=true +resharper_align_multline_type_parameter_constrains=true +resharper_align_multline_type_parameter_list=false +resharper_align_tuple_components=false +resharper_align_union_type_usage=true +resharper_allow_alias=true +resharper_allow_comment_after_lbrace=false +resharper_allow_far_alignment=false +resharper_always_use_end_of_line_brace_style=false +resharper_apply_auto_detected_rules=false +resharper_apply_on_completion=false +resharper_arguments_anonymous_function=positional +resharper_arguments_literal=positional +resharper_arguments_named=positional +resharper_arguments_other=positional +resharper_arguments_skip_single=false +resharper_arguments_string_literal=positional +resharper_attribute_style=do_not_touch +resharper_autodetect_indent_settings=false +resharper_blank_lines_after_block_statements=1 +resharper_blank_lines_after_case=0 +resharper_blank_lines_after_control_transfer_statements=1 +resharper_blank_lines_after_file_scoped_namespace_directive=1 +resharper_blank_lines_after_imports=1 +resharper_blank_lines_after_multiline_statements=0 +resharper_blank_lines_after_options=1 +resharper_blank_lines_after_start_comment=1 +resharper_blank_lines_after_using_list=1 +resharper_blank_lines_around_accessor=0 +resharper_blank_lines_around_auto_property=1 +resharper_blank_lines_around_block_case_section=0 +resharper_blank_lines_around_class_definition=1 +resharper_blank_lines_around_field=1 +resharper_blank_lines_around_function_declaration=0 +resharper_blank_lines_around_function_definition=1 +resharper_blank_lines_around_global_attribute=0 +resharper_blank_lines_around_invocable=1 +resharper_blank_lines_around_local_method=1 +resharper_blank_lines_around_multiline_case_section=0 +resharper_blank_lines_around_namespace=1 +resharper_blank_lines_around_other_declaration=0 +resharper_blank_lines_around_property=1 +resharper_blank_lines_around_razor_functions=1 +resharper_blank_lines_around_razor_helpers=1 +resharper_blank_lines_around_razor_sections=1 +resharper_blank_lines_around_region=1 +resharper_blank_lines_around_single_line_accessor=0 +resharper_blank_lines_around_single_line_auto_property=0 +resharper_blank_lines_around_single_line_field=0 +resharper_blank_lines_around_single_line_function_definition=0 +resharper_blank_lines_around_single_line_invocable=0 +resharper_blank_lines_around_single_line_local_method=0 +resharper_blank_lines_around_single_line_property=0 +resharper_blank_lines_around_single_line_type=0 +resharper_blank_lines_around_type=1 +resharper_blank_lines_before_block_statements=0 +resharper_blank_lines_before_case=0 +resharper_blank_lines_before_control_transfer_statements=0 +resharper_blank_lines_before_multiline_statements=0 +resharper_blank_lines_before_single_line_comment=0 +resharper_blank_lines_inside_namespace=0 +resharper_blank_lines_inside_region=1 +resharper_blank_lines_inside_type=0 +resharper_blank_line_after_pi=true +resharper_braces_for_dowhile=required +resharper_braces_for_fixed=required +resharper_braces_for_for=required_for_multiline +resharper_braces_for_foreach=required_for_multiline +resharper_braces_for_ifelse=not_required_for_both +resharper_braces_for_lock=required +resharper_braces_for_using=required +resharper_braces_for_while=required_for_multiline +resharper_braces_redundant=true +resharper_break_template_declaration=line_break +resharper_can_use_global_alias=true +resharper_configure_await_analysis_mode=disabled +resharper_constructor_or_destructor_body=expression_body +resharper_continuous_indent_multiplier=1 +resharper_continuous_line_indent=single +resharper_cpp_align_multiline_argument=true +resharper_cpp_align_multiline_calls_chain=true +resharper_cpp_align_multiline_extends_list=true +resharper_cpp_align_multiline_for_stmt=true +resharper_cpp_align_multiline_parameter=true +resharper_cpp_align_multiple_declaration=true +resharper_cpp_align_ternary=align_not_nested +resharper_cpp_anonymous_method_declaration_braces=next_line +resharper_cpp_case_block_braces=next_line_shifted_2 +resharper_cpp_empty_block_style=multiline +resharper_cpp_indent_switch_labels=false +resharper_cpp_insert_final_newline=false +resharper_cpp_int_align_comments=false +resharper_cpp_invocable_declaration_braces=next_line +resharper_cpp_max_line_length=120 +resharper_cpp_new_line_before_catch=true +resharper_cpp_new_line_before_else=true +resharper_cpp_new_line_before_while=true +resharper_cpp_other_braces=next_line +resharper_cpp_space_around_binary_operator=true +resharper_cpp_type_declaration_braces=next_line +resharper_cpp_wrap_arguments_style=wrap_if_long +resharper_cpp_wrap_lines=true +resharper_cpp_wrap_parameters_style=wrap_if_long +resharper_csharp_align_multiline_argument=false +resharper_csharp_align_multiline_calls_chain=false +resharper_csharp_align_multiline_expression=false +resharper_csharp_align_multiline_extends_list=false +resharper_csharp_align_multiline_for_stmt=false +resharper_csharp_align_multiline_parameter=false +resharper_csharp_align_multiple_declaration=true +resharper_csharp_empty_block_style=together +resharper_csharp_insert_final_newline=true +resharper_csharp_int_align_comments=true +resharper_csharp_max_line_length=144 +resharper_csharp_naming_rule.enum_member=AaBb +resharper_csharp_naming_rule.method_property_event=AaBb +resharper_csharp_naming_rule.other=AaBb +resharper_csharp_new_line_before_while=false +resharper_csharp_prefer_qualified_reference=false +resharper_csharp_space_after_unary_operator=false +resharper_csharp_wrap_arguments_style=wrap_if_long +resharper_csharp_wrap_before_binary_opsign=true +resharper_csharp_wrap_for_stmt_header_style=wrap_if_long +resharper_csharp_wrap_lines=true +resharper_csharp_wrap_parameters_style=wrap_if_long +resharper_css_brace_style=end_of_line +resharper_css_insert_final_newline=false +resharper_css_keep_blank_lines_between_declarations=1 +resharper_css_max_line_length=120 +resharper_css_wrap_lines=true +resharper_cxxcli_property_declaration_braces=next_line +resharper_declarations_style=separate_lines +resharper_default_exception_variable_name=e +resharper_default_value_when_type_evident=default_literal +resharper_default_value_when_type_not_evident=default_literal +resharper_delete_quotes_from_solid_values=false +resharper_disable_blank_line_changes=false +resharper_disable_formatter=false +resharper_disable_indenter=false +resharper_disable_int_align=false +resharper_disable_line_break_changes=false +resharper_disable_line_break_removal=false +resharper_disable_space_changes=false +resharper_disable_space_changes_before_trailing_comment=false +resharper_dont_remove_extra_blank_lines=false +resharper_enable_wrapping=false +resharper_enforce_line_ending_style=false +resharper_event_handler_pattern_long=$object$On$event$ +resharper_event_handler_pattern_short=On$event$ +resharper_expression_braces=inside +resharper_expression_pars=inside +resharper_extra_spaces=remove_all +resharper_force_attribute_style=separate +resharper_force_chop_compound_do_expression=false +resharper_force_chop_compound_if_expression=false +resharper_force_chop_compound_while_expression=false +resharper_force_control_statements_braces=do_not_change +resharper_force_linebreaks_inside_complex_literals=true +resharper_force_variable_declarations_on_new_line=false +resharper_format_leading_spaces_decl=false +resharper_free_block_braces=next_line +resharper_function_declaration_return_type_style=do_not_change +resharper_function_definition_return_type_style=do_not_change +resharper_generator_mode=false +resharper_html_attribute_indent=align_by_first_attribute +resharper_html_insert_final_newline=false +resharper_html_linebreak_before_elements=body,div,p,form,h1,h2,h3 +resharper_html_max_blank_lines_between_tags=2 +resharper_html_max_line_length=120 +resharper_html_pi_attribute_style=on_single_line +resharper_html_space_before_self_closing=false +resharper_html_wrap_lines=true +resharper_ignore_space_preservation=false +resharper_include_prefix_comment_in_indent=false +resharper_indent_access_specifiers_from_class=false +resharper_indent_aligned_ternary=true +resharper_indent_anonymous_method_block=false +resharper_indent_braces_inside_statement_conditions=true +resharper_indent_case_from_select=true +resharper_indent_child_elements=OneIndent +resharper_indent_class_members_from_access_specifiers=false +resharper_indent_comment=true +resharper_indent_inside_namespace=true +resharper_indent_invocation_pars=inside +resharper_indent_left_par_inside_expression=false +resharper_indent_method_decl_pars=inside +resharper_indent_nested_fixed_stmt=false +resharper_indent_nested_foreach_stmt=true +resharper_indent_nested_for_stmt=true +resharper_indent_nested_lock_stmt=false +resharper_indent_nested_usings_stmt=false +resharper_indent_nested_while_stmt=true +resharper_indent_pars=inside +resharper_indent_preprocessor_directives=none +resharper_indent_preprocessor_if=no_indent +resharper_indent_preprocessor_other=no_indent +resharper_indent_preprocessor_region=usual_indent +resharper_indent_statement_pars=inside +resharper_indent_text=OneIndent +resharper_indent_typearg_angles=inside +resharper_indent_typeparam_angles=inside +resharper_indent_type_constraints=true +resharper_indent_wrapped_function_names=false +resharper_instance_members_qualify_declared_in=this_class, base_class +resharper_int_align=true +resharper_int_align_assignments=true +resharper_int_align_binary_expressions=false +resharper_int_align_declaration_names=false +resharper_int_align_eq=false +resharper_int_align_fields=true +resharper_int_align_fix_in_adjacent=true +resharper_int_align_invocations=true +resharper_int_align_methods=true +resharper_int_align_nested_ternary=true +resharper_int_align_parameters=false +resharper_int_align_properties=true +resharper_int_align_property_patterns=true +resharper_int_align_switch_expressions=true +resharper_int_align_switch_sections=true +resharper_int_align_variables=true +resharper_js_align_multiline_parameter=false +resharper_js_align_multiple_declaration=false +resharper_js_align_ternary=none +resharper_js_brace_style=end_of_line +resharper_js_empty_block_style=multiline +resharper_js_indent_switch_labels=false +resharper_js_insert_final_newline=false +resharper_js_keep_blank_lines_between_declarations=2 +resharper_js_max_line_length=120 +resharper_js_new_line_before_catch=false +resharper_js_new_line_before_else=false +resharper_js_new_line_before_finally=false +resharper_js_new_line_before_while=false +resharper_js_space_around_binary_operator=true +resharper_js_wrap_arguments_style=chop_if_long +resharper_js_wrap_before_binary_opsign=false +resharper_js_wrap_for_stmt_header_style=chop_if_long +resharper_js_wrap_lines=true +resharper_js_wrap_parameters_style=chop_if_long +resharper_keep_blank_lines_in_code=2 +resharper_keep_blank_lines_in_declarations=2 +resharper_keep_existing_attribute_arrangement=false +resharper_keep_existing_declaration_block_arrangement=false +resharper_keep_existing_declaration_parens_arrangement=true +resharper_keep_existing_embedded_arrangement=false +resharper_keep_existing_embedded_block_arrangement=false +resharper_keep_existing_enum_arrangement=false +resharper_keep_existing_expr_member_arrangement=false +resharper_keep_existing_initializer_arrangement=false +resharper_keep_existing_invocation_parens_arrangement=true +resharper_keep_existing_property_patterns_arrangement=true +resharper_keep_existing_switch_expression_arrangement=false +resharper_keep_nontrivial_alias=true +resharper_keep_user_linebreaks=true +resharper_keep_user_wrapping=true +resharper_linebreaks_around_razor_statements=true +resharper_linebreaks_inside_tags_for_elements_longer_than=2147483647 +resharper_linebreaks_inside_tags_for_elements_with_child_elements=true +resharper_linebreaks_inside_tags_for_multiline_elements=true +resharper_linebreak_before_all_elements=false +resharper_linebreak_before_multiline_elements=true +resharper_linebreak_before_singleline_elements=false +resharper_line_break_after_colon_in_member_initializer_lists=do_not_change +resharper_line_break_after_comma_in_member_initializer_lists=false +resharper_line_break_before_comma_in_member_initializer_lists=false +resharper_line_break_before_requires_clause=do_not_change +resharper_linkage_specification_braces=end_of_line +resharper_linkage_specification_indentation=none +resharper_local_function_body=expression_body +resharper_macro_block_begin= +resharper_macro_block_end= +resharper_max_array_initializer_elements_on_line=10000 +resharper_max_attribute_length_for_same_line=38 +resharper_max_enum_members_on_line=1 +resharper_max_formal_parameters_on_line=10000 +resharper_max_initializer_elements_on_line=1 +resharper_max_invocation_arguments_on_line=10000 +resharper_media_query_style=same_line +resharper_member_initializer_list_style=do_not_change +resharper_method_or_operator_body=expression_body +resharper_min_blank_lines_after_imports=0 +resharper_min_blank_lines_around_fields=0 +resharper_min_blank_lines_around_functions=1 +resharper_min_blank_lines_around_types=1 +resharper_min_blank_lines_between_declarations=1 +resharper_namespace_declaration_braces=next_line +resharper_namespace_indentation=all +resharper_nested_ternary_style=autodetect +resharper_new_line_before_enumerators=true +resharper_normalize_tag_names=false +resharper_no_indent_inside_elements=html,body,thead,tbody,tfoot +resharper_no_indent_inside_if_element_longer_than=200 +resharper_object_creation_when_type_evident=target_typed +resharper_object_creation_when_type_not_evident=explicitly_typed +resharper_old_engine=false +resharper_options_braces_pointy=false +resharper_outdent_binary_ops=true +resharper_outdent_binary_pattern_ops=false +resharper_outdent_commas=false +resharper_outdent_dots=false +resharper_outdent_namespace_member=false +resharper_outdent_statement_labels=false +resharper_outdent_ternary_ops=false +resharper_parentheses_non_obvious_operations=none, bitwise, bitwise_inclusive_or, bitwise_exclusive_or, shift, bitwise_and +resharper_parentheses_redundancy_style=remove_if_not_clarifies_precedence +resharper_parentheses_same_type_operations=false +resharper_pi_attributes_indent=align_by_first_attribute +resharper_place_attribute_on_same_line=false +resharper_place_class_decorator_on_the_same_line=false +resharper_place_comments_at_first_column=false +resharper_place_constructor_initializer_on_same_line=false +resharper_place_each_decorator_on_new_line=false +resharper_place_event_attribute_on_same_line=false +resharper_place_expr_accessor_on_single_line=true +resharper_place_expr_method_on_single_line=false +resharper_place_expr_property_on_single_line=false +resharper_place_field_decorator_on_the_same_line=false +resharper_place_linq_into_on_new_line=true +resharper_place_method_decorator_on_the_same_line=false +resharper_place_namespace_definitions_on_same_line=false +resharper_place_property_attribute_on_same_line=false +resharper_place_property_decorator_on_the_same_line=false +resharper_place_simple_case_statement_on_same_line=if_owner_is_single_line +resharper_place_simple_embedded_statement_on_same_line=false +resharper_place_simple_enum_on_single_line=true +resharper_place_simple_initializer_on_single_line=true +resharper_place_simple_property_pattern_on_single_line=true +resharper_place_simple_switch_expression_on_single_line=true +resharper_place_template_args_on_new_line=false +resharper_place_type_constraints_on_same_line=true +resharper_prefer_explicit_discard_declaration=false +resharper_prefer_separate_deconstructed_variables_declaration=false +resharper_preserve_spaces_inside_tags=pre,textarea +resharper_properties_style=separate_lines_for_nonsingle +resharper_protobuf_brace_style=end_of_line +resharper_protobuf_empty_block_style=together_same_line +resharper_protobuf_insert_final_newline=false +resharper_protobuf_max_line_length=120 +resharper_protobuf_wrap_lines=true +resharper_qualified_using_at_nested_scope=false +resharper_quote_style=doublequoted +resharper_razor_prefer_qualified_reference=true +resharper_remove_blank_lines_near_braces=false +resharper_remove_blank_lines_near_braces_in_code=true +resharper_remove_blank_lines_near_braces_in_declarations=true +resharper_remove_this_qualifier=true +resharper_requires_expression_braces=next_line +resharper_resx_attribute_indent=single_indent +resharper_resx_insert_final_newline=false +resharper_resx_linebreak_before_elements= +resharper_resx_max_blank_lines_between_tags=0 +resharper_resx_max_line_length=2147483647 +resharper_resx_pi_attribute_style=do_not_touch +resharper_resx_space_before_self_closing=false +resharper_resx_wrap_lines=false +resharper_resx_wrap_tags_and_pi=false +resharper_resx_wrap_text=false +resharper_selector_style=same_line +resharper_show_autodetect_configure_formatting_tip=true +resharper_simple_blocks=do_not_change +resharper_simple_block_style=do_not_change +resharper_simple_case_statement_style=do_not_change +resharper_simple_embedded_statement_style=do_not_change +resharper_single_statement_function_style=do_not_change +resharper_sort_attributes=false +resharper_sort_class_selectors=false +resharper_sort_usings=true +resharper_sort_usings_lowercase_first=false +resharper_spaces_around_eq_in_attribute=false +resharper_spaces_around_eq_in_pi_attribute=false +resharper_spaces_inside_tags=false +resharper_space_after_arrow=true +resharper_space_after_attributes=true +resharper_space_after_attribute_target_colon=true +resharper_space_after_cast=false +resharper_space_after_colon=true +resharper_space_after_colon_in_case=true +resharper_space_after_colon_in_inheritance_clause=true +resharper_space_after_colon_in_type_annotation=true +resharper_space_after_comma=true +resharper_space_after_for_colon=true +resharper_space_after_function_comma=true +resharper_space_after_keywords_in_control_flow_statements=true +resharper_space_after_last_attribute=false +resharper_space_after_last_pi_attribute=false +resharper_space_after_media_colon=true +resharper_space_after_media_comma=true +resharper_space_after_operator_keyword=true +resharper_space_after_property_colon=true +resharper_space_after_property_semicolon=true +resharper_space_after_ptr_in_data_member=true +resharper_space_after_ptr_in_data_members=false +resharper_space_after_ptr_in_method=true +resharper_space_after_ref_in_data_member=true +resharper_space_after_ref_in_data_members=false +resharper_space_after_ref_in_method=true +resharper_space_after_selector_comma=true +resharper_space_after_semicolon_in_for_statement=true +resharper_space_after_separator=false +resharper_space_after_ternary_colon=true +resharper_space_after_ternary_quest=true +resharper_space_after_triple_slash=true +resharper_space_after_type_parameter_constraint_colon=true +resharper_space_around_additive_op=true +resharper_space_around_alias_eq=true +resharper_space_around_assignment_op=true +resharper_space_around_assignment_operator=true +resharper_space_around_attribute_match_operator=false +resharper_space_around_deref_in_trailing_return_type=true +resharper_space_around_lambda_arrow=true +resharper_space_around_member_access_operator=false +resharper_space_around_operator=true +resharper_space_around_pipe_or_amper_in_type_usage=true +resharper_space_around_relational_op=true +resharper_space_around_selector_operator=true +resharper_space_around_shift_op=true +resharper_space_around_stmt_colon=true +resharper_space_around_ternary_operator=true +resharper_space_before_array_rank_parentheses=false +resharper_space_before_arrow=true +resharper_space_before_attribute_target_colon=false +resharper_space_before_checked_parentheses=false +resharper_space_before_colon=false +resharper_space_before_colon_in_case=false +resharper_space_before_colon_in_inheritance_clause=true +resharper_space_before_colon_in_type_annotation=false +resharper_space_before_comma=false +resharper_space_before_default_parentheses=false +resharper_space_before_empty_invocation_parentheses=false +resharper_space_before_empty_method_parentheses=false +resharper_space_before_for_colon=true +resharper_space_before_function_comma=false +resharper_space_before_initializer_braces=false +resharper_space_before_invocation_parentheses=false +resharper_space_before_label_colon=false +resharper_space_before_lambda_parentheses=false +resharper_space_before_media_colon=false +resharper_space_before_media_comma=false +resharper_space_before_method_parentheses=false +resharper_space_before_nameof_parentheses=false +resharper_space_before_new_parentheses=false +resharper_space_before_nullable_mark=false +resharper_space_before_open_square_brackets=false +resharper_space_before_pointer_asterik_declaration=false +resharper_space_before_property_colon=false +resharper_space_before_property_semicolon=false +resharper_space_before_ptr_in_abstract_decl=false +resharper_space_before_ptr_in_data_member=false +resharper_space_before_ptr_in_data_members=true +resharper_space_before_ptr_in_method=false +resharper_space_before_ref_in_abstract_decl=false +resharper_space_before_ref_in_data_member=false +resharper_space_before_ref_in_data_members=true +resharper_space_before_ref_in_method=false +resharper_space_before_selector_comma=false +resharper_space_before_semicolon=false +resharper_space_before_semicolon_in_for_statement=false +resharper_space_before_separator=false +resharper_space_before_singleline_accessorholder=true +resharper_space_before_sizeof_parentheses=false +resharper_space_before_template_args=false +resharper_space_before_template_params=true +resharper_space_before_ternary_colon=true +resharper_space_before_ternary_quest=true +resharper_space_before_trailing_comment=true +resharper_space_before_typeof_parentheses=false +resharper_space_before_type_argument_angle=false +resharper_space_before_type_parameters_brackets=false +resharper_space_before_type_parameter_angle=false +resharper_space_before_type_parameter_constraint_colon=true +resharper_space_before_type_parameter_parentheses=true +resharper_space_between_accessors_in_singleline_property=true +resharper_space_between_attribute_sections=true +resharper_space_between_closing_angle_brackets_in_template_args=false +resharper_space_between_empty_square_brackets=false +resharper_space_between_keyword_and_expression=true +resharper_space_between_keyword_and_type=true +resharper_space_between_method_call_empty_parameter_list_parentheses=false +resharper_space_between_method_call_name_and_opening_parenthesis=false +resharper_space_between_method_call_parameter_list_parentheses=false +resharper_space_between_method_declaration_empty_parameter_list_parentheses=false +resharper_space_between_method_declaration_name_and_open_parenthesis=false +resharper_space_between_method_declaration_parameter_list_parentheses=false +resharper_space_between_parentheses_of_control_flow_statements=false +resharper_space_between_square_brackets=false +resharper_space_between_typecast_parentheses=false +resharper_space_colon_after=true +resharper_space_colon_before=false +resharper_space_comma=true +resharper_space_equals=true +resharper_space_inside_braces=true +resharper_space_in_singleline_accessorholder=true +resharper_space_in_singleline_anonymous_method=true +resharper_space_in_singleline_method=true +resharper_space_near_postfix_and_prefix_op=false +resharper_space_within_array_initialization_braces=false +resharper_space_within_array_rank_empty_parentheses=false +resharper_space_within_array_rank_parentheses=false +resharper_space_within_attribute_angles=false +resharper_space_within_attribute_match_brackets=false +resharper_space_within_checked_parentheses=false +resharper_space_within_default_parentheses=false +resharper_space_within_empty_braces=true +resharper_space_within_empty_initializer_braces=false +resharper_space_within_empty_invocation_parentheses=false +resharper_space_within_empty_method_parentheses=false +resharper_space_within_empty_object_literal_braces=false +resharper_space_within_empty_template_params=false +resharper_space_within_expression_parentheses=false +resharper_space_within_function_parentheses=false +resharper_space_within_import_braces=true +resharper_space_within_initializer_braces=false +resharper_space_within_invocation_parentheses=false +resharper_space_within_media_block=true +resharper_space_within_media_parentheses=false +resharper_space_within_method_parentheses=false +resharper_space_within_nameof_parentheses=false +resharper_space_within_new_parentheses=false +resharper_space_within_object_literal_braces=true +resharper_space_within_parentheses=false +resharper_space_within_property_block=true +resharper_space_within_single_line_array_initializer_braces=true +resharper_space_within_sizeof_parentheses=false +resharper_space_within_template_args=false +resharper_space_within_template_argument=false +resharper_space_within_template_params=false +resharper_space_within_tuple_parentheses=false +resharper_space_within_typeof_parentheses=false +resharper_space_within_type_argument_angles=false +resharper_space_within_type_parameters_brackets=false +resharper_space_within_type_parameter_angles=false +resharper_space_within_type_parameter_parentheses=false +resharper_special_else_if_treatment=true +resharper_static_members_qualify_members=none +resharper_static_members_qualify_with=declared_type +resharper_stick_comment=true +resharper_support_vs_event_naming_pattern=true +resharper_termination_style=ensure_semicolon +resharper_toplevel_function_declaration_return_type_style=do_not_change +resharper_toplevel_function_definition_return_type_style=do_not_change +resharper_trailing_comma_in_multiline_lists=true +resharper_trailing_comma_in_singleline_lists=false +resharper_types_braces=end_of_line +resharper_use_continuous_indent_inside_initializer_braces=true +resharper_use_continuous_indent_inside_parens=true +resharper_use_continuous_line_indent_in_expression_braces=false +resharper_use_continuous_line_indent_in_method_pars=false +resharper_use_heuristics_for_body_style=true +resharper_use_indents_from_main_language_in_file=true +resharper_use_indent_from_previous_element=true +resharper_use_indent_from_vs=false +resharper_use_roslyn_logic_for_evident_types=false +resharper_vb_align_multiline_argument=true +resharper_vb_align_multiline_expression=true +resharper_vb_align_multiline_parameter=true +resharper_vb_align_multiple_declaration=true +resharper_vb_insert_final_newline=false +resharper_vb_max_line_length=120 +resharper_vb_place_field_attribute_on_same_line=true +resharper_vb_place_method_attribute_on_same_line=false +resharper_vb_place_type_attribute_on_same_line=false +resharper_vb_prefer_qualified_reference=false +resharper_vb_space_after_unary_operator=true +resharper_vb_space_around_multiplicative_op=false +resharper_vb_wrap_arguments_style=wrap_if_long +resharper_vb_wrap_before_binary_opsign=false +resharper_vb_wrap_lines=true +resharper_vb_wrap_parameters_style=wrap_if_long +resharper_wrap_after_binary_opsign=true +resharper_wrap_after_declaration_lpar=false +resharper_wrap_after_dot=false +resharper_wrap_after_dot_in_method_calls=false +resharper_wrap_after_expression_lbrace=true +resharper_wrap_after_invocation_lpar=false +resharper_wrap_around_elements=true +resharper_wrap_array_initializer_style=chop_always +resharper_wrap_array_literals=chop_if_long +resharper_wrap_base_clause_style=wrap_if_long +resharper_wrap_before_arrow_with_expressions=true +resharper_wrap_before_binary_pattern_op=true +resharper_wrap_before_colon=false +resharper_wrap_before_comma=false +resharper_wrap_before_comma_in_base_clause=false +resharper_wrap_before_declaration_lpar=false +resharper_wrap_before_declaration_rpar=false +resharper_wrap_before_dot=true +resharper_wrap_before_eq=false +resharper_wrap_before_expression_rbrace=true +resharper_wrap_before_extends_colon=false +resharper_wrap_before_first_type_parameter_constraint=false +resharper_wrap_before_invocation_lpar=false +resharper_wrap_before_invocation_rpar=false +resharper_wrap_before_linq_expression=false +resharper_wrap_before_ternary_opsigns=true +resharper_wrap_before_type_parameter_langle=false +resharper_wrap_braced_init_list_style=wrap_if_long +resharper_wrap_chained_binary_expressions=chop_if_long +resharper_wrap_chained_binary_patterns=wrap_if_long +resharper_wrap_chained_method_calls=wrap_if_long +resharper_wrap_ctor_initializer_style=wrap_if_long +resharper_wrap_enumeration_style=chop_if_long +resharper_wrap_enum_declaration=chop_always +resharper_wrap_enum_style=do_not_change +resharper_wrap_extends_list_style=wrap_if_long +resharper_wrap_imports=chop_if_long +resharper_wrap_multiple_declaration_style=chop_if_long +resharper_wrap_multiple_type_parameter_constraints_style=chop_if_long +resharper_wrap_object_literals=chop_if_long +resharper_wrap_property_pattern=chop_if_long +resharper_wrap_switch_expression=chop_always +resharper_wrap_ternary_expr_style=chop_if_long +resharper_wrap_union_type_usage=chop_if_long +resharper_wrap_verbatim_interpolated_strings=no_wrap +resharper_xmldoc_attribute_indent=single_indent +resharper_xmldoc_insert_final_newline=false +resharper_xmldoc_linebreak_before_elements=summary,remarks,example,returns,param,typeparam,value,para +resharper_xmldoc_max_blank_lines_between_tags=0 +resharper_xmldoc_max_line_length=120 +resharper_xmldoc_pi_attribute_style=do_not_touch +resharper_xmldoc_space_before_self_closing=true +resharper_xmldoc_wrap_lines=true +resharper_xmldoc_wrap_tags_and_pi=true +resharper_xmldoc_wrap_text=true +resharper_xml_attribute_indent=align_by_first_attribute +resharper_xml_insert_final_newline=false +resharper_xml_linebreak_before_elements= +resharper_xml_max_blank_lines_between_tags=2 +resharper_xml_max_line_length=120 +resharper_xml_pi_attribute_style=do_not_touch +resharper_xml_space_before_self_closing=true +resharper_xml_wrap_lines=true +resharper_xml_wrap_tags_and_pi=true +resharper_xml_wrap_text=false + +# ReSharper inspection severities +resharper_abstract_class_constructor_can_be_made_protected_highlighting=hint +resharper_access_rights_in_text_highlighting=warning +resharper_access_to_disposed_closure_highlighting=warning +resharper_access_to_for_each_variable_in_closure_highlighting=warning +resharper_access_to_modified_closure_highlighting=warning +resharper_access_to_static_member_via_derived_type_highlighting=warning +resharper_address_of_marshal_by_ref_object_highlighting=warning +resharper_amd_dependency_path_problem_highlighting=none +resharper_amd_external_module_highlighting=suggestion +resharper_angular_html_banana_highlighting=warning +resharper_annotate_can_be_null_parameter_highlighting=none +resharper_annotate_can_be_null_type_member_highlighting=none +resharper_annotate_not_null_parameter_highlighting=none +resharper_annotate_not_null_type_member_highlighting=none +resharper_annotation_conflict_in_hierarchy_highlighting=warning +resharper_annotation_redundancy_at_value_type_highlighting=warning +resharper_annotation_redundancy_in_hierarchy_highlighting=warning +resharper_arguments_style_anonymous_function_highlighting=hint +resharper_arguments_style_literal_highlighting=hint +resharper_arguments_style_named_expression_highlighting=hint +resharper_arguments_style_other_highlighting=hint +resharper_arguments_style_string_literal_highlighting=hint +resharper_arrange_accessor_owner_body_highlighting=suggestion +resharper_arrange_attributes_highlighting=none +resharper_arrange_constructor_or_destructor_body_highlighting=hint +resharper_arrange_default_value_when_type_evident_highlighting=suggestion +resharper_arrange_default_value_when_type_not_evident_highlighting=hint +resharper_arrange_local_function_body_highlighting=hint +resharper_arrange_method_or_operator_body_highlighting=hint +resharper_arrange_missing_parentheses_highlighting=hint +resharper_arrange_namespace_body_highlighting=hint +resharper_arrange_object_creation_when_type_evident_highlighting=suggestion +resharper_arrange_object_creation_when_type_not_evident_highlighting=hint +resharper_arrange_redundant_parentheses_highlighting=hint +resharper_arrange_static_member_qualifier_highlighting=hint +resharper_arrange_this_qualifier_highlighting=hint +resharper_arrange_trailing_comma_in_multiline_lists_highlighting=hint +resharper_arrange_trailing_comma_in_singleline_lists_highlighting=hint +resharper_arrange_type_member_modifiers_highlighting=hint +resharper_arrange_type_modifiers_highlighting=hint +resharper_arrange_var_keywords_in_deconstructing_declaration_highlighting=suggestion +resharper_asp_content_placeholder_not_resolved_highlighting=error +resharper_asp_custom_page_parser_filter_type_highlighting=warning +resharper_asp_dead_code_highlighting=warning +resharper_asp_entity_highlighting=warning +resharper_asp_image_highlighting=warning +resharper_asp_invalid_control_type_highlighting=error +resharper_asp_not_resolved_highlighting=error +resharper_asp_ods_method_reference_resolve_error_highlighting=error +resharper_asp_resolve_warning_highlighting=warning +resharper_asp_skin_not_resolved_highlighting=error +resharper_asp_tag_attribute_with_optional_value_highlighting=warning +resharper_asp_theme_not_resolved_highlighting=error +resharper_asp_unused_register_directive_highlighting_highlighting=warning +resharper_asp_warning_highlighting=warning +resharper_assigned_value_is_never_used_highlighting=warning +resharper_assigned_value_wont_be_assigned_to_corresponding_field_highlighting=warning +resharper_assignment_in_conditional_expression_highlighting=warning +resharper_assignment_in_condition_expression_highlighting=warning +resharper_assignment_is_fully_discarded_highlighting=warning +resharper_assign_null_to_not_null_attribute_highlighting=warning +resharper_assign_to_constant_highlighting=error +resharper_assign_to_implicit_global_in_function_scope_highlighting=warning +resharper_asxx_path_error_highlighting=warning +resharper_async_iterator_invocation_without_await_foreach_highlighting=warning +resharper_async_void_lambda_highlighting=warning +resharper_async_void_method_highlighting=none +resharper_auto_property_can_be_made_get_only_global_highlighting=suggestion +resharper_auto_property_can_be_made_get_only_local_highlighting=suggestion +resharper_bad_attribute_brackets_spaces_highlighting=none +resharper_bad_braces_spaces_highlighting=none +resharper_bad_child_statement_indent_highlighting=warning +resharper_bad_colon_spaces_highlighting=none +resharper_bad_comma_spaces_highlighting=none +resharper_bad_control_braces_indent_highlighting=suggestion +resharper_bad_control_braces_line_breaks_highlighting=none +resharper_bad_declaration_braces_indent_highlighting=none +resharper_bad_declaration_braces_line_breaks_highlighting=none +resharper_bad_empty_braces_line_breaks_highlighting=none +resharper_bad_expression_braces_indent_highlighting=none +resharper_bad_expression_braces_line_breaks_highlighting=none +resharper_bad_generic_brackets_spaces_highlighting=none +resharper_bad_indent_highlighting=none +resharper_bad_linq_line_breaks_highlighting=none +resharper_bad_list_line_breaks_highlighting=none +resharper_bad_member_access_spaces_highlighting=none +resharper_bad_namespace_braces_indent_highlighting=none +resharper_bad_parens_line_breaks_highlighting=none +resharper_bad_parens_spaces_highlighting=none +resharper_bad_preprocessor_indent_highlighting=none +resharper_bad_semicolon_spaces_highlighting=none +resharper_bad_spaces_after_keyword_highlighting=none +resharper_bad_square_brackets_spaces_highlighting=none +resharper_bad_switch_braces_indent_highlighting=none +resharper_bad_symbol_spaces_highlighting=none +resharper_base_member_has_params_highlighting=warning +resharper_base_method_call_with_default_parameter_highlighting=warning +resharper_base_object_equals_is_object_equals_highlighting=warning +resharper_base_object_get_hash_code_call_in_get_hash_code_highlighting=warning +resharper_bitwise_operator_on_enum_without_flags_highlighting=warning +resharper_block_scope_redeclaration_highlighting=error +resharper_built_in_type_reference_style_for_member_access_highlighting=hint +resharper_built_in_type_reference_style_highlighting=hint +resharper_by_ref_argument_is_volatile_field_highlighting=warning +resharper_caller_callee_using_error_highlighting=error +resharper_caller_callee_using_highlighting=warning +resharper_cannot_apply_equality_operator_to_type_highlighting=warning +resharper_center_tag_is_obsolete_highlighting=warning +resharper_check_for_reference_equality_instead_1_highlighting=suggestion +resharper_check_for_reference_equality_instead_2_highlighting=suggestion +resharper_check_for_reference_equality_instead_3_highlighting=suggestion +resharper_check_for_reference_equality_instead_4_highlighting=suggestion +resharper_check_namespace_highlighting=warning +resharper_class_cannot_be_instantiated_highlighting=warning +resharper_class_can_be_sealed_global_highlighting=none +resharper_class_can_be_sealed_local_highlighting=none +resharper_class_highlighting=suggestion +resharper_class_never_instantiated_global_highlighting=suggestion +resharper_class_never_instantiated_local_highlighting=suggestion +resharper_class_with_virtual_members_never_inherited_global_highlighting=suggestion +resharper_class_with_virtual_members_never_inherited_local_highlighting=suggestion +resharper_clear_attribute_is_obsolete_all_highlighting=warning +resharper_clear_attribute_is_obsolete_highlighting=warning +resharper_closure_on_modified_variable_highlighting=warning +resharper_coerced_equals_using_highlighting=warning +resharper_coerced_equals_using_with_null_undefined_highlighting=none +resharper_collection_never_queried_global_highlighting=warning +resharper_collection_never_queried_local_highlighting=warning +resharper_collection_never_updated_global_highlighting=warning +resharper_collection_never_updated_local_highlighting=warning +resharper_comma_not_valid_here_highlighting=error +resharper_comment_typo_highlighting=suggestion +resharper_common_js_external_module_highlighting=suggestion +resharper_compare_non_constrained_generic_with_null_highlighting=none +resharper_compare_of_floats_by_equality_operator_highlighting=none +resharper_conditional_ternary_equal_branch_highlighting=warning +resharper_condition_is_always_const_highlighting=warning +resharper_condition_is_always_true_or_false_highlighting=warning +resharper_confusing_char_as_integer_in_constructor_highlighting=warning +resharper_constant_conditional_access_qualifier_highlighting=warning +resharper_constant_null_coalescing_condition_highlighting=warning +resharper_constructor_call_not_used_highlighting=warning +resharper_constructor_initializer_loop_highlighting=warning +resharper_container_annotation_redundancy_highlighting=warning +resharper_context_value_is_provided_highlighting=none +resharper_contract_annotation_not_parsed_highlighting=warning +resharper_convert_closure_to_method_group_highlighting=suggestion +resharper_convert_conditional_ternary_expression_to_switch_expression_highlighting=hint +resharper_convert_if_do_to_while_highlighting=suggestion +resharper_convert_if_statement_to_conditional_ternary_expression_highlighting=suggestion +resharper_convert_if_statement_to_null_coalescing_assignment_highlighting=suggestion +resharper_convert_if_statement_to_null_coalescing_expression_highlighting=suggestion +resharper_convert_if_statement_to_return_statement_highlighting=hint +resharper_convert_if_statement_to_switch_expression_highlighting=hint +resharper_convert_if_statement_to_switch_statement_highlighting=hint +resharper_convert_if_to_or_expression_highlighting=suggestion +resharper_convert_nullable_to_short_form_highlighting=suggestion +resharper_convert_switch_statement_to_switch_expression_highlighting=hint +resharper_convert_to_auto_property_highlighting=suggestion +resharper_convert_to_auto_property_when_possible_highlighting=hint +resharper_convert_to_auto_property_with_private_setter_highlighting=hint +resharper_convert_to_compound_assignment_highlighting=hint +resharper_convert_to_constant_global_highlighting=hint +resharper_convert_to_constant_local_highlighting=hint +resharper_convert_to_lambda_expression_highlighting=suggestion +resharper_convert_to_lambda_expression_when_possible_highlighting=none +resharper_convert_to_local_function_highlighting=suggestion +resharper_convert_to_null_coalescing_compound_assignment_highlighting=suggestion +resharper_convert_to_primary_constructor_highlighting=suggestion +resharper_convert_to_static_class_highlighting=suggestion +resharper_convert_to_using_declaration_highlighting=suggestion +resharper_convert_to_vb_auto_property_highlighting=suggestion +resharper_convert_to_vb_auto_property_when_possible_highlighting=hint +resharper_convert_to_vb_auto_property_with_private_setter_highlighting=hint +resharper_convert_type_check_pattern_to_null_check_highlighting=warning +resharper_convert_type_check_to_null_check_highlighting=warning +resharper_co_variant_array_conversion_highlighting=warning +resharper_cpp_abstract_class_without_specifier_highlighting=warning +resharper_cpp_abstract_final_class_highlighting=warning +resharper_cpp_abstract_virtual_function_call_in_ctor_highlighting=error +resharper_cpp_access_specifier_with_no_declarations_highlighting=suggestion +resharper_cpp_assigned_value_is_never_used_highlighting=warning +resharper_cpp_awaiter_type_is_not_class_highlighting=warning +resharper_cpp_bad_angle_brackets_spaces_highlighting=none +resharper_cpp_bad_braces_spaces_highlighting=none +resharper_cpp_bad_child_statement_indent_highlighting=none +resharper_cpp_bad_colon_spaces_highlighting=none +resharper_cpp_bad_comma_spaces_highlighting=none +resharper_cpp_bad_control_braces_indent_highlighting=none +resharper_cpp_bad_control_braces_line_breaks_highlighting=none +resharper_cpp_bad_declaration_braces_indent_highlighting=none +resharper_cpp_bad_declaration_braces_line_breaks_highlighting=none +resharper_cpp_bad_empty_braces_line_breaks_highlighting=none +resharper_cpp_bad_expression_braces_indent_highlighting=none +resharper_cpp_bad_expression_braces_line_breaks_highlighting=none +resharper_cpp_bad_indent_highlighting=none +resharper_cpp_bad_list_line_breaks_highlighting=none +resharper_cpp_bad_member_access_spaces_highlighting=none +resharper_cpp_bad_namespace_braces_indent_highlighting=none +resharper_cpp_bad_parens_line_breaks_highlighting=none +resharper_cpp_bad_parens_spaces_highlighting=none +resharper_cpp_bad_semicolon_spaces_highlighting=none +resharper_cpp_bad_spaces_after_keyword_highlighting=none +resharper_cpp_bad_square_brackets_spaces_highlighting=none +resharper_cpp_bad_switch_braces_indent_highlighting=none +resharper_cpp_bad_symbol_spaces_highlighting=none +resharper_cpp_boolean_increment_expression_highlighting=warning +resharper_cpp_boost_format_bad_code_highlighting=warning +resharper_cpp_boost_format_legacy_code_highlighting=suggestion +resharper_cpp_boost_format_mixed_args_highlighting=error +resharper_cpp_boost_format_too_few_args_highlighting=error +resharper_cpp_boost_format_too_many_args_highlighting=warning +resharper_cpp_clang_tidy_abseil_duration_addition_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_comparison_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_conversion_cast_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_division_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_factory_float_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_factory_scale_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_subtraction_highlighting=none +resharper_cpp_clang_tidy_abseil_duration_unnecessary_conversion_highlighting=none +resharper_cpp_clang_tidy_abseil_faster_strsplit_delimiter_highlighting=none +resharper_cpp_clang_tidy_abseil_no_internal_dependencies_highlighting=none +resharper_cpp_clang_tidy_abseil_no_namespace_highlighting=none +resharper_cpp_clang_tidy_abseil_redundant_strcat_calls_highlighting=none +resharper_cpp_clang_tidy_abseil_string_find_startswith_highlighting=none +resharper_cpp_clang_tidy_abseil_string_find_str_contains_highlighting=none +resharper_cpp_clang_tidy_abseil_str_cat_append_highlighting=none +resharper_cpp_clang_tidy_abseil_time_comparison_highlighting=none +resharper_cpp_clang_tidy_abseil_time_subtraction_highlighting=none +resharper_cpp_clang_tidy_abseil_upgrade_duration_conversions_highlighting=none +resharper_cpp_clang_tidy_altera_id_dependent_backward_branch_highlighting=none +resharper_cpp_clang_tidy_altera_kernel_name_restriction_highlighting=none +resharper_cpp_clang_tidy_altera_single_work_item_barrier_highlighting=none +resharper_cpp_clang_tidy_altera_struct_pack_align_highlighting=none +resharper_cpp_clang_tidy_altera_unroll_loops_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_accept4_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_accept_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_creat_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_dup_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_epoll_create1_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_epoll_create_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_fopen_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_inotify_init1_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_inotify_init_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_memfd_create_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_open_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_pipe2_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_pipe_highlighting=none +resharper_cpp_clang_tidy_android_cloexec_socket_highlighting=none +resharper_cpp_clang_tidy_android_comparison_in_temp_failure_retry_highlighting=none +resharper_cpp_clang_tidy_boost_use_to_string_highlighting=suggestion +resharper_cpp_clang_tidy_bugprone_argument_comment_highlighting=suggestion +resharper_cpp_clang_tidy_bugprone_assert_side_effect_highlighting=warning +resharper_cpp_clang_tidy_bugprone_bad_signal_to_kill_thread_highlighting=warning +resharper_cpp_clang_tidy_bugprone_bool_pointer_implicit_conversion_highlighting=none +resharper_cpp_clang_tidy_bugprone_branch_clone_highlighting=warning +resharper_cpp_clang_tidy_bugprone_copy_constructor_init_highlighting=warning +resharper_cpp_clang_tidy_bugprone_dangling_handle_highlighting=warning +resharper_cpp_clang_tidy_bugprone_dynamic_static_initializers_highlighting=warning +resharper_cpp_clang_tidy_bugprone_easily_swappable_parameters_highlighting=none +resharper_cpp_clang_tidy_bugprone_exception_escape_highlighting=none +resharper_cpp_clang_tidy_bugprone_fold_init_type_highlighting=warning +resharper_cpp_clang_tidy_bugprone_forwarding_reference_overload_highlighting=warning +resharper_cpp_clang_tidy_bugprone_forward_declaration_namespace_highlighting=warning +resharper_cpp_clang_tidy_bugprone_implicit_widening_of_multiplication_result_highlighting=warning +resharper_cpp_clang_tidy_bugprone_inaccurate_erase_highlighting=warning +resharper_cpp_clang_tidy_bugprone_incorrect_roundings_highlighting=warning +resharper_cpp_clang_tidy_bugprone_infinite_loop_highlighting=warning +resharper_cpp_clang_tidy_bugprone_integer_division_highlighting=warning +resharper_cpp_clang_tidy_bugprone_lambda_function_name_highlighting=warning +resharper_cpp_clang_tidy_bugprone_macro_parentheses_highlighting=warning +resharper_cpp_clang_tidy_bugprone_macro_repeated_side_effects_highlighting=warning +resharper_cpp_clang_tidy_bugprone_misplaced_operator_in_strlen_in_alloc_highlighting=warning +resharper_cpp_clang_tidy_bugprone_misplaced_pointer_arithmetic_in_alloc_highlighting=warning +resharper_cpp_clang_tidy_bugprone_misplaced_widening_cast_highlighting=warning +resharper_cpp_clang_tidy_bugprone_move_forwarding_reference_highlighting=warning +resharper_cpp_clang_tidy_bugprone_multiple_statement_macro_highlighting=warning +resharper_cpp_clang_tidy_bugprone_narrowing_conversions_highlighting=warning +resharper_cpp_clang_tidy_bugprone_not_null_terminated_result_highlighting=warning +resharper_cpp_clang_tidy_bugprone_no_escape_highlighting=warning +resharper_cpp_clang_tidy_bugprone_parent_virtual_call_highlighting=warning +resharper_cpp_clang_tidy_bugprone_posix_return_highlighting=warning +resharper_cpp_clang_tidy_bugprone_redundant_branch_condition_highlighting=warning +resharper_cpp_clang_tidy_bugprone_reserved_identifier_highlighting=warning +resharper_cpp_clang_tidy_bugprone_signal_handler_highlighting=warning +resharper_cpp_clang_tidy_bugprone_signed_char_misuse_highlighting=warning +resharper_cpp_clang_tidy_bugprone_sizeof_container_highlighting=warning +resharper_cpp_clang_tidy_bugprone_sizeof_expression_highlighting=warning +resharper_cpp_clang_tidy_bugprone_spuriously_wake_up_functions_highlighting=warning +resharper_cpp_clang_tidy_bugprone_string_constructor_highlighting=warning +resharper_cpp_clang_tidy_bugprone_string_integer_assignment_highlighting=warning +resharper_cpp_clang_tidy_bugprone_string_literal_with_embedded_nul_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_enum_usage_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_include_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_memset_usage_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_missing_comma_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_semicolon_highlighting=warning +resharper_cpp_clang_tidy_bugprone_suspicious_string_compare_highlighting=warning +resharper_cpp_clang_tidy_bugprone_swapped_arguments_highlighting=warning +resharper_cpp_clang_tidy_bugprone_terminating_continue_highlighting=warning +resharper_cpp_clang_tidy_bugprone_throw_keyword_missing_highlighting=warning +resharper_cpp_clang_tidy_bugprone_too_small_loop_variable_highlighting=warning +resharper_cpp_clang_tidy_bugprone_undefined_memory_manipulation_highlighting=warning +resharper_cpp_clang_tidy_bugprone_undelegated_constructor_highlighting=warning +resharper_cpp_clang_tidy_bugprone_unhandled_exception_at_new_highlighting=none +resharper_cpp_clang_tidy_bugprone_unhandled_self_assignment_highlighting=warning +resharper_cpp_clang_tidy_bugprone_unused_raii_highlighting=warning +resharper_cpp_clang_tidy_bugprone_unused_return_value_highlighting=warning +resharper_cpp_clang_tidy_bugprone_use_after_move_highlighting=warning +resharper_cpp_clang_tidy_bugprone_virtual_near_miss_highlighting=suggestion +resharper_cpp_clang_tidy_cert_con36_c_highlighting=none +resharper_cpp_clang_tidy_cert_con54_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl03_c_highlighting=none +resharper_cpp_clang_tidy_cert_dcl16_c_highlighting=none +resharper_cpp_clang_tidy_cert_dcl21_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl37_c_highlighting=none +resharper_cpp_clang_tidy_cert_dcl50_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl51_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl54_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_dcl58_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_dcl59_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_env33_c_highlighting=none +resharper_cpp_clang_tidy_cert_err09_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_err34_c_highlighting=suggestion +resharper_cpp_clang_tidy_cert_err52_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_err58_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_err60_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_err61_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_fio38_c_highlighting=none +resharper_cpp_clang_tidy_cert_flp30_c_highlighting=warning +resharper_cpp_clang_tidy_cert_mem57_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_msc30_c_highlighting=none +resharper_cpp_clang_tidy_cert_msc32_c_highlighting=none +resharper_cpp_clang_tidy_cert_msc50_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_msc51_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_oop11_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_oop54_cpp_highlighting=none +resharper_cpp_clang_tidy_cert_oop57_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_oop58_cpp_highlighting=warning +resharper_cpp_clang_tidy_cert_pos44_c_highlighting=none +resharper_cpp_clang_tidy_cert_pos47_c_highlighting=none +resharper_cpp_clang_tidy_cert_sig30_c_highlighting=none +resharper_cpp_clang_tidy_cert_str34_c_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_google_g_test_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_llvm_cast_value_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_llvm_return_value_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_std_c_library_functions_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_api_modeling_trust_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_builtin_builtin_functions_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_builtin_no_return_functions_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_call_and_message_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_call_and_message_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_divide_zero_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_dynamic_type_propagation_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_nonnil_string_constants_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_non_null_param_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_null_dereference_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_stack_address_escape_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_stack_addr_escape_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_undefined_binary_operator_result_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_array_subscript_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_assign_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_branch_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_captured_block_variable_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_uninitialized_undef_return_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_core_vla_size_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_inner_pointer_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_move_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_new_delete_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_new_delete_leaks_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_placement_new_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_pure_virtual_call_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_self_assignment_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_smart_ptr_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_cplusplus_virtual_call_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_deadcode_dead_stores_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_fuchsia_handle_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullability_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_dereferenced_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_passed_to_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_nullable_returned_from_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_null_passed_to_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_nullability_null_returned_from_nonnull_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_cplusplus_uninitialized_object_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_cplusplus_virtual_call_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_mpi_mpi_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_osx_cocoa_localizability_empty_localization_context_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_osx_cocoa_localizability_non_localized_string_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_osx_os_object_c_style_cast_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_performance_gcd_antipattern_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_performance_padding_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_optin_portability_unix_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_at_sync_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_autorelease_write_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_class_release_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_dealloc_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_incompatible_method_types_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_loops_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_missing_super_call_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_nil_arg_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_non_nil_return_value_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_ns_autorelease_pool_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_ns_error_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_obj_c_generics_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_retain_count_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_retain_count_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_run_loop_autorelease_leak_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_self_init_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_super_dealloc_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_unused_ivars_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_cocoa_variadic_method_types_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_error_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_number_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_cf_retain_release_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_containers_out_of_bounds_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_core_foundation_containers_pointer_sized_values_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_mig_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_ns_or_cf_error_deref_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_number_object_conversion_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_obj_c_property_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_os_object_retain_count_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_osx_sec_keychain_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_float_loop_counter_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bcmp_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bcopy_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_bzero_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_decode_value_of_obj_c_type_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_deprecated_or_unsafe_buffer_handling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_getpw_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_gets_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_mkstemp_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_mktemp_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_rand_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_security_syntax_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_strcpy_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_unchecked_return_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_security_insecure_api_vfork_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_api_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_bad_size_arg_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_c_string_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_cstring_null_arg_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_dynamic_memory_modeling_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_malloc_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_malloc_sizeof_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_mismatched_deallocator_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_unix_vfork_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_copy_to_self_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_uninitialized_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_unterminated_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_valist_valist_base_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_webkit_no_uncounted_member_checker_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_webkit_ref_cntbl_base_virtual_dtor_highlighting=none +resharper_cpp_clang_tidy_clang_analyzer_webkit_uncounted_lambda_captures_checker_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_absolute_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_abstract_final_class_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_abstract_vbase_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_address_of_packed_member_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_address_of_temporary_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_aix_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_align_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_alloca_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_alloca_with_align_alignof_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_delete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_ellipsis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_member_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ambiguous_reversed_operator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_analyzer_incompatible_plugin_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_anonymous_pack_parens_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_anon_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_bridge_casts_disallowed_in_nonarc_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_maybe_repeated_use_of_weak_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_non_pod_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_perform_selector_leaks_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_repeated_use_of_weak_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_retain_cycles_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_arc_unsafe_retained_assign_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_argument_outside_range_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_array_bounds_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_array_bounds_pointer_arithmetic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_asm_operand_widths_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_assign_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_assume_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atimport_in_framework_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atomic_alignment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atomic_implicit_seq_cst_highlighting=suggestion +resharper_cpp_clang_tidy_clang_diagnostic_atomic_memory_ordering_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_atomic_property_with_user_defined_accessor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_attribute_packed_for_bitfield_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_at_protocol_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_disable_vptr_sanitizer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_import_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_storage_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_auto_var_id_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_availability_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_avr_rtlib_linking_quirks_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_backslash_newline_escape_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bad_function_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_binding_in_condition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bind_to_temporary_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitfield_constant_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitfield_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitfield_width_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitwise_conditional_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bitwise_op_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_block_capture_autoreleasing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bool_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bool_operation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_braced_scalar_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_bridge_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_builtin_assume_aligned_alignment_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_builtin_macro_redefined_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_builtin_memcpy_chk_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_builtin_requires_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c11_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c2x_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c99_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_c99_designator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_c99_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_called_once_parameter_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_call_to_pure_virtual_from_ctor_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_align_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_calling_convention_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_function_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_of_sel_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_qual_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cast_qual_unrelated_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cf_string_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_char_subscripts_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_clang_cl_pch_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_class_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_class_varargs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cmse_union_leak_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_comma_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_comment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compare_distinct_pointer_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_completion_handler_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_complex_component_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_by_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_by_space_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_compound_token_split_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_concepts_ts_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_conditional_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_conditional_uninitialized_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_config_macros_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constant_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constant_evaluated_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constant_logical_operand_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_constexpr_not_const_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_consumed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_coroutine_missing_unhandled_exception_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_covered_switch_default_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_deprecated_writable_strings_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_compat_reserved_user_defined_literal_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_extra_semi_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_inline_namespace_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_long_long_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp11_narrowing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_binary_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp14_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_mangling_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp17_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_designator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp20_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp2a_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp2a_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp2b_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_bind_to_temporary_copy_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_extra_semi_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_local_type_template_args_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_compat_unnamed_type_template_args_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_binary_literal_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_cpp17_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp98_cpp11_cpp14_cpp17_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cpp_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cstring_format_directive_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ctad_maybe_unsupported_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ctu_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_cuda_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_custom_atomic_properties_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_cxx_attribute_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_else_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_gsl_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dangling_initializer_list_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_darwin_sdk_settings_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_date_time_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dealloc_in_category_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_debug_compression_unavailable_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_declaration_after_statement_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_defaulted_function_deleted_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_delegating_ctor_cycles_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_abstract_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_incomplete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_non_abstract_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_delete_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_altivec_src_compat_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_anon_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_array_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_attributes_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_comma_subscript_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_user_provided_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_copy_with_user_provided_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_declarations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_dynamic_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_compare_conditional_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_enum_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_implementations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_increment_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_isa_usage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_pointer_introspection_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_objc_pointer_introspection_perform_selector_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_register_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_this_capture_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_deprecated_volatile_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_direct_ivar_access_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_disabled_macro_expansion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_distributed_object_modifiers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_division_by_zero_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dllexport_explicit_instantiation_decl_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dllimport_static_field_def_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dll_attribute_on_redeclaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_deprecated_sync_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_html_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_documentation_unknown_command_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_dollar_in_identifier_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_double_promotion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dtor_name_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dtor_typedef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_decl_specifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_method_arg_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_method_match_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_duplicate_protocol_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dynamic_class_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_dynamic_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_embedded_directive_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_body_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_decomposition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_init_stmt_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_empty_translation_unit_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_encode_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_conditional_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_compare_switch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_enum_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_enum_too_large_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_error_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_exceptions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_excess_initializers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_exit_time_destructors_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_expansion_to_defined_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_explicit_initialize_call_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_explicit_ownership_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_export_unnamed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_export_using_directive_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extern_c_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_extern_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_qualification_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_semi_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_semi_stmt_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_extra_tokens_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_final_dtor_non_final_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_fixed_enum_extension_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_fixed_point_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_flag_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_flexible_array_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_equal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_overflow_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_float_zero_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_extra_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_insufficient_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_invalid_specifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_nonliteral_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_non_iso_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_security_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_type_confusion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_format_zero_length_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_fortify_source_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_for_loop_analysis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_four_char_constants_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_framework_include_private_from_public_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_frame_address_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_frame_larger_than_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_free_nonheap_object_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_function_def_in_objc_container_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_function_multiversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gcc_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_global_constructors_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_global_isel_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_alignof_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_anonymous_struct_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_gnu_array_member_paren_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_auto_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_binary_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_case_range_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_complex_integer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_compound_literal_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_conditional_omitted_operand_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_designator_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_empty_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_empty_struct_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_flexible_array_initializer_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_flexible_array_union_member_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_folding_constant_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_imaginary_constant_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_include_next_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_inline_cpp_without_extern_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_label_as_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_redeclared_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_statement_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_static_float_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_string_literal_operator_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_union_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_variable_sized_type_not_at_end_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_gnu_zero_variadic_macro_arguments_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_header_guard_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_header_hygiene_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_hip_only_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_idiomatic_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_attributes_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ignored_availability_without_sdk_settings_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ignored_optimization_argument_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragmas_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragma_intrinsic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ignored_pragma_optimize_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_ignored_qualifiers_highlighting=suggestion +resharper_cpp_clang_tidy_clang_diagnostic_implicitly_unsigned_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_atomic_properties_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_const_int_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_conversion_floating_point_to_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_exception_spec_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_fallthrough_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_fallthrough_per_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_fixed_point_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_function_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_float_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_int_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_implicit_retain_self_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_import_preprocessor_directive_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inaccessible_base_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_include_next_absolute_path_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_include_next_outside_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_function_pointer_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_library_redeclaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_ms_struct_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_pointer_types_discards_qualifiers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_pointer_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_property_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incompatible_sysroot_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_framework_module_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_implementation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_setjmp_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_incomplete_umbrella_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_dllimport_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_missing_destructor_override_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inconsistent_missing_override_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_increment_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_independent_class_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_infinite_recursion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_initializer_overrides_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_injected_class_name_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inline_asm_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inline_namespace_reopened_noninline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_inline_new_delete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_instantiation_after_specialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_integer_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_interrupt_service_routine_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_in_bool_context_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_to_pointer_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_int_to_void_pointer_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_constexpr_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_iboutlet_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_initializer_from_system_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_ios_deployment_target_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_noreturn_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_no_builtin_names_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_offsetof_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_or_nonexistent_directory_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_partial_specialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_pp_token_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_source_encoding_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_invalid_token_paste_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_jump_seh_finally_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_keyword_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_keyword_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_knr_promoted_parameter_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_language_extension_token_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_large_by_value_copy_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_literal_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_literal_range_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_local_type_template_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_logical_not_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_logical_op_parentheses_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_long_long_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_macro_redefined_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_main_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_main_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_malformed_warning_check_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_many_braces_around_scalar_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_max_tokens_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_max_unsigned_zero_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_memset_transposed_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_memsize_comparison_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_method_signatures_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_abstract_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_anon_tag_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_charize_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_comment_paste_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_const_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_cpp_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_default_arg_redefinition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_drectve_section_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_end_of_file_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_enum_forward_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_enum_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_exists_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_explicit_constructor_call_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_extra_qualification_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_fixed_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_flexible_array_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_goto_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_inaccessible_base_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_include_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_mutable_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_pure_definition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_redeclare_static_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_sealed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_static_assert_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_template_shadow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_union_member_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_unqualified_friend_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_using_decl_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_microsoft_void_pseudo_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_misleading_indentation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_new_delete_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_parameter_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_return_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_mismatched_tags_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_braces_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_constinit_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_declarations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_field_initializers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_method_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_noescape_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_noreturn_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_prototypes_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_missing_prototype_for_cc_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_selector_name_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_sysroot_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_missing_variable_declarations_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_misspelled_assumption_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_modules_ambiguous_internal_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_modules_import_nested_redundant_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_conflict_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_file_config_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_file_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_module_import_in_extern_c_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_msvc_not_found_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_multichar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_multiple_move_vbase_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nested_anon_types_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_newline_eof_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_new_returns_null_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_noderef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonnull_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonportable_include_path_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonportable_system_include_path_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nonportable_vector_initialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nontrivial_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_c_typedef_for_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_literal_null_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_modular_include_in_framework_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_modular_include_in_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_pod_varargs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_power_of_two_alignment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_non_virtual_dtor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nsconsumed_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nsreturns_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ns_object_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_completeness_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_completeness_on_arrays_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_declspec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullability_inferred_on_nested_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_nullable_to_nonnull_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_arithmetic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_character_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_dereference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_pointer_arithmetic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_null_pointer_subtraction_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_odr_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_old_style_cast_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_opencl_unsupported_rgba_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp51_extensions_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_clauses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_loop_form_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_mapping_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_openmp_target_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_option_ignored_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_ordered_compare_function_pointers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_out_of_line_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_out_of_scope_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overlength_strings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overloaded_shift_op_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overloaded_virtual_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_override_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_override_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overriding_method_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_overriding_t_option_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_over_aligned_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_packed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_padded_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_parentheses_equality_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pass_failed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pch_date_time_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pedantic_core_features_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pedantic_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pessimizing_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_arith_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_bool_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_integer_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_sign_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_to_enum_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_to_int_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pointer_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_poison_system_directories_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_potentially_evaluated_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragmas_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_clang_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_messages_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_once_outside_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_pack_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_pack_suspicious_include_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pragma_system_header_outside_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_predefined_identifier_outside_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_pre_c2x_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_c2x_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp14_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp14_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp17_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp17_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp20_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp20_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp2b_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_cpp2b_compat_pedantic_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_pre_openmp51_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_private_extern_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_private_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_private_module_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_missing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_out_of_date_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_profile_instr_unprofiled_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_property_access_dot_syntax_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_property_attribute_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_protocol_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_protocol_property_synthesis_ambiguity_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_psabi_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_qualified_void_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_quoted_include_in_framework_header_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_range_loop_analysis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_range_loop_bind_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_range_loop_construct_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_readonly_iboutlet_property_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_receiver_expr_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_receiver_forward_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_redeclared_class_member_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_redundant_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_redundant_parens_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_register_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reinterpret_base_class_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reorder_ctor_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reorder_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reorder_init_list_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_requires_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_requires_super_attribute_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_identifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_id_macro_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_macro_identifier_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_reserved_user_defined_literal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_retained_language_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_stack_address_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_std_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_type_c_linkage_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_return_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_rewrite_not_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_section_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_selector_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_selector_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_assign_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_assign_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_assign_overloaded_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_self_move_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_semicolon_before_method_body_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sentinel_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_serialized_diagnostics_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_in_constructor_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_shadow_field_in_constructor_modified_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_shadow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_ivar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shadow_uncaptured_local_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_shift_count_negative_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_count_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_negative_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_op_parentheses_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shift_sign_overflow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_shorten64_to32_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_signed_enum_bitfield_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_signed_unsigned_wchar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sign_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sign_conversion_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_argument_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_decay_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_array_div_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_pointer_div_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sizeof_pointer_memaccess_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_slash_u_filename_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_slh_asm_goto_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_sometimes_uninitialized_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_source_uses_openmp_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_spir_compat_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_static_float_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_inline_explicit_instantiation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_in_inline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_local_in_inline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_static_self_init_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_stdlibcxx_not_found_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_strict_prototypes_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_strict_selector_match_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_concatenation_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_plus_char_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_string_plus_int_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_strlcpy_strlcat_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_strncat_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_suggest_destructor_override_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_suggest_override_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_super_class_method_mismatch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_suspicious_bzero_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_switch_bool_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_switch_enum_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_switch_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_sync_fetch_and_nand_semantics_changed_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_bitwise_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_in_range_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_constant_out_of_range_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_objc_bool_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_overlap_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_pointer_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_type_limit_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_undefined_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_char_zero_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_enum_zero_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tautological_unsigned_zero_compare_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_tautological_value_range_compare_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_tentative_definition_incomplete_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_analysis_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_attributes_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_beta_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_negative_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_precise_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_thread_safety_verbose_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_trigraphs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_typedef_redefinition_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_typename_missing_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_type_safety_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unable_to_open_stats_file_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unavailable_declarations_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undeclared_selector_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_bool_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_func_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_inline_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_internal_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_internal_type_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_reinterpret_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undefined_var_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_undef_prefix_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_underaligned_exception_object_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unevaluated_expression_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unguarded_availability_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unguarded_availability_new_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_homoglyph_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_whitespace_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unicode_zero_width_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_uninitialized_const_reference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_uninitialized_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_argument_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_attributes_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unknown_cuda_version_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unknown_escape_sequence_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_pragmas_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unknown_sanitizers_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unknown_warning_option_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unnamed_type_template_args_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unneeded_internal_declaration_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unneeded_member_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_break_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_loop_increment_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unreachable_code_return_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsequenced_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_abs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_availability_guard_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_cb_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_dll_base_class_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_friend_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_gpopt_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_nan_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_target_opt_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unsupported_visibility_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unusable_partial_specialization_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_but_set_parameter_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_but_set_variable_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_comparison_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_const_variable_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_exception_parameter_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_getter_return_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_label_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_lambda_capture_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_local_typedef_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_macros_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_member_function_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_parameter_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_private_field_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_property_ivar_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_result_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_template_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_value_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_unused_variable_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_unused_volatile_lvalue_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_used_but_marked_unused_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_user_defined_literals_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_user_defined_warnings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_varargs_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_variadic_macros_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vector_conversion_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vec_elem_size_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vexing_parse_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_visibility_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vla_extension_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_vla_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_void_pointer_to_enum_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_void_pointer_to_int_cast_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_void_ptr_dereference_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_warnings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_wasm_exception_spec_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_weak_template_vtables_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_weak_vtables_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_writable_strings_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_xor_used_as_pow_highlighting=warning +resharper_cpp_clang_tidy_clang_diagnostic_zero_as_null_pointer_constant_highlighting=none +resharper_cpp_clang_tidy_clang_diagnostic_zero_length_array_highlighting=warning +resharper_cpp_clang_tidy_concurrency_mt_unsafe_highlighting=warning +resharper_cpp_clang_tidy_concurrency_thread_canceltype_asynchronous_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_c_arrays_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_goto_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_magic_numbers_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_avoid_non_const_global_variables_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_c_copy_assignment_signature_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_explicit_virtual_functions_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_init_variables_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_interfaces_global_init_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_macro_usage_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_narrowing_conversions_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_non_private_member_variables_in_classes_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_no_malloc_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_owning_memory_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_prefer_member_initializer_highlighting=suggestion +resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_array_to_pointer_decay_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_constant_array_index_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_bounds_pointer_arithmetic_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_const_cast_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_cstyle_cast_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_member_init_highlighting=warning +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_reinterpret_cast_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_static_cast_downcast_highlighting=suggestion +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_union_access_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_pro_type_vararg_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_slicing_highlighting=none +resharper_cpp_clang_tidy_cppcoreguidelines_special_member_functions_highlighting=suggestion +resharper_cpp_clang_tidy_darwin_avoid_spinlock_highlighting=none +resharper_cpp_clang_tidy_darwin_dispatch_once_nonstatic_highlighting=none +resharper_cpp_clang_tidy_fuchsia_default_arguments_calls_highlighting=none +resharper_cpp_clang_tidy_fuchsia_default_arguments_declarations_highlighting=none +resharper_cpp_clang_tidy_fuchsia_header_anon_namespaces_highlighting=none +resharper_cpp_clang_tidy_fuchsia_multiple_inheritance_highlighting=none +resharper_cpp_clang_tidy_fuchsia_overloaded_operator_highlighting=none +resharper_cpp_clang_tidy_fuchsia_statically_constructed_objects_highlighting=none +resharper_cpp_clang_tidy_fuchsia_trailing_return_highlighting=none +resharper_cpp_clang_tidy_fuchsia_virtual_inheritance_highlighting=none +resharper_cpp_clang_tidy_google_build_explicit_make_pair_highlighting=none +resharper_cpp_clang_tidy_google_build_namespaces_highlighting=none +resharper_cpp_clang_tidy_google_build_using_namespace_highlighting=none +resharper_cpp_clang_tidy_google_default_arguments_highlighting=none +resharper_cpp_clang_tidy_google_explicit_constructor_highlighting=none +resharper_cpp_clang_tidy_google_global_names_in_headers_highlighting=none +resharper_cpp_clang_tidy_google_objc_avoid_nsobject_new_highlighting=none +resharper_cpp_clang_tidy_google_objc_avoid_throwing_exception_highlighting=none +resharper_cpp_clang_tidy_google_objc_function_naming_highlighting=none +resharper_cpp_clang_tidy_google_objc_global_variable_declaration_highlighting=none +resharper_cpp_clang_tidy_google_readability_avoid_underscore_in_googletest_name_highlighting=none +resharper_cpp_clang_tidy_google_readability_braces_around_statements_highlighting=none +resharper_cpp_clang_tidy_google_readability_casting_highlighting=none +resharper_cpp_clang_tidy_google_readability_function_size_highlighting=none +resharper_cpp_clang_tidy_google_readability_namespace_comments_highlighting=none +resharper_cpp_clang_tidy_google_readability_todo_highlighting=none +resharper_cpp_clang_tidy_google_runtime_int_highlighting=none +resharper_cpp_clang_tidy_google_runtime_operator_highlighting=warning +resharper_cpp_clang_tidy_google_upgrade_googletest_case_highlighting=suggestion +resharper_cpp_clang_tidy_hicpp_avoid_c_arrays_highlighting=none +resharper_cpp_clang_tidy_hicpp_avoid_goto_highlighting=warning +resharper_cpp_clang_tidy_hicpp_braces_around_statements_highlighting=none +resharper_cpp_clang_tidy_hicpp_deprecated_headers_highlighting=none +resharper_cpp_clang_tidy_hicpp_exception_baseclass_highlighting=suggestion +resharper_cpp_clang_tidy_hicpp_explicit_conversions_highlighting=none +resharper_cpp_clang_tidy_hicpp_function_size_highlighting=none +resharper_cpp_clang_tidy_hicpp_invalid_access_moved_highlighting=none +resharper_cpp_clang_tidy_hicpp_member_init_highlighting=none +resharper_cpp_clang_tidy_hicpp_move_const_arg_highlighting=none +resharper_cpp_clang_tidy_hicpp_multiway_paths_covered_highlighting=warning +resharper_cpp_clang_tidy_hicpp_named_parameter_highlighting=none +resharper_cpp_clang_tidy_hicpp_new_delete_operators_highlighting=none +resharper_cpp_clang_tidy_hicpp_noexcept_move_highlighting=none +resharper_cpp_clang_tidy_hicpp_no_array_decay_highlighting=none +resharper_cpp_clang_tidy_hicpp_no_assembler_highlighting=none +resharper_cpp_clang_tidy_hicpp_no_malloc_highlighting=none +resharper_cpp_clang_tidy_hicpp_signed_bitwise_highlighting=none +resharper_cpp_clang_tidy_hicpp_special_member_functions_highlighting=none +resharper_cpp_clang_tidy_hicpp_static_assert_highlighting=none +resharper_cpp_clang_tidy_hicpp_undelegated_constructor_highlighting=none +resharper_cpp_clang_tidy_hicpp_uppercase_literal_suffix_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_auto_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_emplace_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_equals_default_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_equals_delete_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_noexcept_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_nullptr_highlighting=none +resharper_cpp_clang_tidy_hicpp_use_override_highlighting=none +resharper_cpp_clang_tidy_hicpp_vararg_highlighting=none +resharper_cpp_clang_tidy_highlighting_highlighting=suggestion +resharper_cpp_clang_tidy_linuxkernel_must_check_errs_highlighting=warning +resharper_cpp_clang_tidy_llvmlibc_callee_namespace_highlighting=none +resharper_cpp_clang_tidy_llvmlibc_implementation_in_namespace_highlighting=none +resharper_cpp_clang_tidy_llvmlibc_restrict_system_libc_headers_highlighting=none +resharper_cpp_clang_tidy_llvm_else_after_return_highlighting=none +resharper_cpp_clang_tidy_llvm_header_guard_highlighting=none +resharper_cpp_clang_tidy_llvm_include_order_highlighting=none +resharper_cpp_clang_tidy_llvm_namespace_comment_highlighting=none +resharper_cpp_clang_tidy_llvm_prefer_isa_or_dyn_cast_in_conditionals_highlighting=none +resharper_cpp_clang_tidy_llvm_prefer_register_over_unsigned_highlighting=suggestion +resharper_cpp_clang_tidy_llvm_qualified_auto_highlighting=none +resharper_cpp_clang_tidy_llvm_twine_local_highlighting=none +resharper_cpp_clang_tidy_misc_definitions_in_headers_highlighting=none +resharper_cpp_clang_tidy_misc_misplaced_const_highlighting=warning +resharper_cpp_clang_tidy_misc_new_delete_overloads_highlighting=warning +resharper_cpp_clang_tidy_misc_non_copyable_objects_highlighting=warning +resharper_cpp_clang_tidy_misc_non_private_member_variables_in_classes_highlighting=none +resharper_cpp_clang_tidy_misc_no_recursion_highlighting=none +resharper_cpp_clang_tidy_misc_redundant_expression_highlighting=warning +resharper_cpp_clang_tidy_misc_static_assert_highlighting=suggestion +resharper_cpp_clang_tidy_misc_throw_by_value_catch_by_reference_highlighting=warning +resharper_cpp_clang_tidy_misc_unconventional_assign_operator_highlighting=warning +resharper_cpp_clang_tidy_misc_uniqueptr_reset_release_highlighting=suggestion +resharper_cpp_clang_tidy_misc_unused_alias_decls_highlighting=suggestion +resharper_cpp_clang_tidy_misc_unused_parameters_highlighting=none +resharper_cpp_clang_tidy_misc_unused_using_decls_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_avoid_bind_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_avoid_c_arrays_highlighting=none +resharper_cpp_clang_tidy_modernize_concat_nested_namespaces_highlighting=none +resharper_cpp_clang_tidy_modernize_deprecated_headers_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_deprecated_ios_base_aliases_highlighting=warning +resharper_cpp_clang_tidy_modernize_loop_convert_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_make_shared_highlighting=none +resharper_cpp_clang_tidy_modernize_make_unique_highlighting=none +resharper_cpp_clang_tidy_modernize_pass_by_value_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_raw_string_literal_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_redundant_void_arg_highlighting=none +resharper_cpp_clang_tidy_modernize_replace_auto_ptr_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_replace_disallow_copy_and_assign_macro_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_replace_random_shuffle_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_return_braced_init_list_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_shrink_to_fit_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_unary_static_assert_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_auto_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_bool_literals_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_default_member_init_highlighting=none +resharper_cpp_clang_tidy_modernize_use_emplace_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_equals_default_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_equals_delete_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_nodiscard_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_noexcept_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_nullptr_highlighting=none +resharper_cpp_clang_tidy_modernize_use_override_highlighting=none +resharper_cpp_clang_tidy_modernize_use_trailing_return_type_highlighting=none +resharper_cpp_clang_tidy_modernize_use_transparent_functors_highlighting=suggestion +resharper_cpp_clang_tidy_modernize_use_uncaught_exceptions_highlighting=warning +resharper_cpp_clang_tidy_modernize_use_using_highlighting=none +resharper_cpp_clang_tidy_mpi_buffer_deref_highlighting=warning +resharper_cpp_clang_tidy_mpi_type_mismatch_highlighting=warning +resharper_cpp_clang_tidy_objc_avoid_nserror_init_highlighting=warning +resharper_cpp_clang_tidy_objc_dealloc_in_category_highlighting=warning +resharper_cpp_clang_tidy_objc_forbidden_subclassing_highlighting=warning +resharper_cpp_clang_tidy_objc_missing_hash_highlighting=warning +resharper_cpp_clang_tidy_objc_nsinvocation_argument_lifetime_highlighting=warning +resharper_cpp_clang_tidy_objc_property_declaration_highlighting=warning +resharper_cpp_clang_tidy_objc_super_self_highlighting=warning +resharper_cpp_clang_tidy_openmp_exception_escape_highlighting=warning +resharper_cpp_clang_tidy_openmp_use_default_none_highlighting=warning +resharper_cpp_clang_tidy_performance_faster_string_find_highlighting=suggestion +resharper_cpp_clang_tidy_performance_for_range_copy_highlighting=suggestion +resharper_cpp_clang_tidy_performance_implicit_conversion_in_loop_highlighting=suggestion +resharper_cpp_clang_tidy_performance_inefficient_algorithm_highlighting=suggestion +resharper_cpp_clang_tidy_performance_inefficient_string_concatenation_highlighting=suggestion +resharper_cpp_clang_tidy_performance_inefficient_vector_operation_highlighting=suggestion +resharper_cpp_clang_tidy_performance_move_constructor_init_highlighting=warning +resharper_cpp_clang_tidy_performance_move_const_arg_highlighting=suggestion +resharper_cpp_clang_tidy_performance_noexcept_move_constructor_highlighting=none +resharper_cpp_clang_tidy_performance_no_automatic_move_highlighting=warning +resharper_cpp_clang_tidy_performance_no_int_to_ptr_highlighting=warning +resharper_cpp_clang_tidy_performance_trivially_destructible_highlighting=suggestion +resharper_cpp_clang_tidy_performance_type_promotion_in_math_fn_highlighting=suggestion +resharper_cpp_clang_tidy_performance_unnecessary_copy_initialization_highlighting=suggestion +resharper_cpp_clang_tidy_performance_unnecessary_value_param_highlighting=suggestion +resharper_cpp_clang_tidy_portability_restrict_system_includes_highlighting=none +resharper_cpp_clang_tidy_portability_simd_intrinsics_highlighting=none +resharper_cpp_clang_tidy_readability_avoid_const_params_in_decls_highlighting=none +resharper_cpp_clang_tidy_readability_braces_around_statements_highlighting=none +resharper_cpp_clang_tidy_readability_const_return_type_highlighting=none +resharper_cpp_clang_tidy_readability_container_size_empty_highlighting=suggestion +resharper_cpp_clang_tidy_readability_convert_member_functions_to_static_highlighting=none +resharper_cpp_clang_tidy_readability_delete_null_pointer_highlighting=suggestion +resharper_cpp_clang_tidy_readability_else_after_return_highlighting=none +resharper_cpp_clang_tidy_readability_function_cognitive_complexity_highlighting=none +resharper_cpp_clang_tidy_readability_function_size_highlighting=none +resharper_cpp_clang_tidy_readability_identifier_naming_highlighting=none +resharper_cpp_clang_tidy_readability_implicit_bool_conversion_highlighting=none +resharper_cpp_clang_tidy_readability_inconsistent_declaration_parameter_name_highlighting=suggestion +resharper_cpp_clang_tidy_readability_isolate_declaration_highlighting=none +resharper_cpp_clang_tidy_readability_magic_numbers_highlighting=none +resharper_cpp_clang_tidy_readability_make_member_function_const_highlighting=none +resharper_cpp_clang_tidy_readability_misleading_indentation_highlighting=none +resharper_cpp_clang_tidy_readability_misplaced_array_index_highlighting=suggestion +resharper_cpp_clang_tidy_readability_named_parameter_highlighting=none +resharper_cpp_clang_tidy_readability_non_const_parameter_highlighting=none +resharper_cpp_clang_tidy_readability_qualified_auto_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_access_specifiers_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_control_flow_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_declaration_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_function_ptr_dereference_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_member_init_highlighting=none +resharper_cpp_clang_tidy_readability_redundant_preprocessor_highlighting=warning +resharper_cpp_clang_tidy_readability_redundant_smartptr_get_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_string_cstr_highlighting=suggestion +resharper_cpp_clang_tidy_readability_redundant_string_init_highlighting=suggestion +resharper_cpp_clang_tidy_readability_simplify_boolean_expr_highlighting=none +resharper_cpp_clang_tidy_readability_simplify_subscript_expr_highlighting=warning +resharper_cpp_clang_tidy_readability_static_accessed_through_instance_highlighting=suggestion +resharper_cpp_clang_tidy_readability_static_definition_in_anonymous_namespace_highlighting=none +resharper_cpp_clang_tidy_readability_string_compare_highlighting=warning +resharper_cpp_clang_tidy_readability_suspicious_call_argument_highlighting=warning +resharper_cpp_clang_tidy_readability_uniqueptr_delete_release_highlighting=suggestion +resharper_cpp_clang_tidy_readability_uppercase_literal_suffix_highlighting=none +resharper_cpp_clang_tidy_readability_use_anyofallof_highlighting=suggestion +resharper_cpp_clang_tidy_zircon_temporary_objects_highlighting=none +resharper_cpp_class_can_be_final_highlighting=hint +resharper_cpp_class_disallow_lazy_merging_highlighting=warning +resharper_cpp_class_is_incomplete_highlighting=warning +resharper_cpp_class_needs_constructor_because_of_uninitialized_member_highlighting=warning +resharper_cpp_class_never_used_highlighting=warning +resharper_cpp_compile_time_constant_can_be_replaced_with_boolean_constant_highlighting=suggestion +resharper_cpp_const_parameter_in_declaration_highlighting=suggestion +resharper_cpp_const_value_function_return_type_highlighting=suggestion +resharper_cpp_coroutine_call_resolve_error_highlighting=warning +resharper_cpp_cv_qualifier_can_not_be_applied_to_reference_highlighting=warning +resharper_cpp_c_style_cast_highlighting=suggestion +resharper_cpp_declaration_hides_local_highlighting=warning +resharper_cpp_declaration_hides_uncaptured_local_highlighting=hint +resharper_cpp_declaration_specifier_without_declarators_highlighting=warning +resharper_cpp_declarator_disambiguated_as_function_highlighting=warning +resharper_cpp_declarator_never_used_highlighting=warning +resharper_cpp_declarator_used_before_initialization_highlighting=error +resharper_cpp_defaulted_special_member_function_is_implicitly_deleted_highlighting=warning +resharper_cpp_default_case_not_handled_in_switch_statement_highlighting=warning +resharper_cpp_default_initialization_with_no_user_constructor_highlighting=warning +resharper_cpp_default_is_used_as_identifier_highlighting=warning +resharper_cpp_deleting_void_pointer_highlighting=warning +resharper_cpp_dependent_template_without_template_keyword_highlighting=warning +resharper_cpp_dependent_type_without_typename_keyword_highlighting=warning +resharper_cpp_deprecated_entity_highlighting=warning +resharper_cpp_deprecated_register_storage_class_specifier_highlighting=warning +resharper_cpp_dereference_operator_limit_exceeded_highlighting=warning +resharper_cpp_discarded_postfix_operator_result_highlighting=suggestion +resharper_cpp_doxygen_syntax_error_highlighting=warning +resharper_cpp_doxygen_undocumented_parameter_highlighting=suggestion +resharper_cpp_doxygen_unresolved_reference_highlighting=warning +resharper_cpp_empty_declaration_highlighting=warning +resharper_cpp_enforce_cv_qualifiers_order_highlighting=none +resharper_cpp_enforce_cv_qualifiers_placement_highlighting=none +resharper_cpp_enforce_do_statement_braces_highlighting=none +resharper_cpp_enforce_for_statement_braces_highlighting=none +resharper_cpp_enforce_function_declaration_style_highlighting=none +resharper_cpp_enforce_if_statement_braces_highlighting=none +resharper_cpp_enforce_nested_namespaces_style_highlighting=hint +resharper_cpp_enforce_overriding_destructor_style_highlighting=suggestion +resharper_cpp_enforce_overriding_function_style_highlighting=suggestion +resharper_cpp_enforce_type_alias_code_style_highlighting=none +resharper_cpp_enforce_while_statement_braces_highlighting=none +resharper_cpp_entity_assigned_but_no_read_highlighting=warning +resharper_cpp_entity_used_only_in_unevaluated_context_highlighting=warning +resharper_cpp_enumerator_never_used_highlighting=warning +resharper_cpp_equal_operands_in_binary_expression_highlighting=warning +resharper_cpp_explicit_specialization_in_non_namespace_scope_highlighting=warning +resharper_cpp_expression_without_side_effects_highlighting=warning +resharper_cpp_final_function_in_final_class_highlighting=suggestion +resharper_cpp_final_non_overriding_virtual_function_highlighting=suggestion +resharper_cpp_for_loop_can_be_replaced_with_while_highlighting=suggestion +resharper_cpp_functional_style_cast_highlighting=suggestion +resharper_cpp_function_doesnt_return_value_highlighting=warning +resharper_cpp_function_is_not_implemented_highlighting=warning +resharper_cpp_header_has_been_already_included_highlighting=hint +resharper_cpp_hidden_function_highlighting=warning +resharper_cpp_hiding_function_highlighting=warning +resharper_cpp_identical_operands_in_binary_expression_highlighting=warning +resharper_cpp_if_can_be_replaced_by_constexpr_if_highlighting=suggestion +resharper_cpp_implicit_default_constructor_not_available_highlighting=warning +resharper_cpp_incompatible_pointer_conversion_highlighting=warning +resharper_cpp_incomplete_switch_statement_highlighting=warning +resharper_cpp_inconsistent_naming_highlighting=hint +resharper_cpp_incorrect_blank_lines_near_braces_highlighting=none +resharper_cpp_initialized_value_is_always_rewritten_highlighting=warning +resharper_cpp_integral_to_pointer_conversion_highlighting=warning +resharper_cpp_invalid_line_continuation_highlighting=warning +resharper_cpp_join_declaration_and_assignment_highlighting=suggestion +resharper_cpp_lambda_capture_never_used_highlighting=warning +resharper_cpp_local_variable_may_be_const_highlighting=suggestion +resharper_cpp_local_variable_might_not_be_initialized_highlighting=warning +resharper_cpp_local_variable_with_non_trivial_dtor_is_never_used_highlighting=none +resharper_cpp_long_float_highlighting=warning +resharper_cpp_member_function_may_be_const_highlighting=suggestion +resharper_cpp_member_function_may_be_static_highlighting=suggestion +resharper_cpp_member_initializers_order_highlighting=suggestion +resharper_cpp_mismatched_class_tags_highlighting=warning +resharper_cpp_missing_blank_lines_highlighting=none +resharper_cpp_missing_include_guard_highlighting=warning +resharper_cpp_missing_indent_highlighting=none +resharper_cpp_missing_keyword_throw_highlighting=warning +resharper_cpp_missing_linebreak_highlighting=none +resharper_cpp_missing_space_highlighting=none +resharper_cpp_ms_ext_address_of_class_r_value_highlighting=warning +resharper_cpp_ms_ext_binding_r_value_to_lvalue_reference_highlighting=warning +resharper_cpp_ms_ext_copy_elision_in_copy_init_declarator_highlighting=warning +resharper_cpp_ms_ext_double_user_conversion_in_copy_init_highlighting=warning +resharper_cpp_ms_ext_not_initialized_static_const_local_var_highlighting=warning +resharper_cpp_ms_ext_reinterpret_cast_from_nullptr_highlighting=warning +resharper_cpp_multiple_spaces_highlighting=none +resharper_cpp_must_be_public_virtual_to_implement_interface_highlighting=warning +resharper_cpp_mutable_specifier_on_reference_member_highlighting=warning +resharper_cpp_nodiscard_function_without_return_value_highlighting=warning +resharper_cpp_non_exception_safe_resource_acquisition_highlighting=hint +resharper_cpp_non_explicit_conversion_operator_highlighting=hint +resharper_cpp_non_explicit_converting_constructor_highlighting=hint +resharper_cpp_non_inline_function_definition_in_header_file_highlighting=warning +resharper_cpp_non_inline_variable_definition_in_header_file_highlighting=warning +resharper_cpp_not_all_paths_return_value_highlighting=warning +resharper_cpp_no_discard_expression_highlighting=warning +resharper_cpp_object_member_might_not_be_initialized_highlighting=warning +resharper_cpp_outdent_is_off_prev_level_highlighting=none +resharper_cpp_out_parameter_must_be_written_highlighting=warning +resharper_cpp_parameter_may_be_const_highlighting=hint +resharper_cpp_parameter_may_be_const_ptr_or_ref_highlighting=suggestion +resharper_cpp_parameter_names_mismatch_highlighting=hint +resharper_cpp_parameter_never_used_highlighting=hint +resharper_cpp_parameter_value_is_reassigned_highlighting=warning +resharper_cpp_pointer_conversion_drops_qualifiers_highlighting=warning +resharper_cpp_pointer_to_integral_conversion_highlighting=warning +resharper_cpp_polymorphic_class_with_non_virtual_public_destructor_highlighting=warning +resharper_cpp_possibly_erroneous_empty_statements_highlighting=warning +resharper_cpp_possibly_uninitialized_member_highlighting=warning +resharper_cpp_possibly_unintended_object_slicing_highlighting=warning +resharper_cpp_precompiled_header_is_not_included_highlighting=error +resharper_cpp_precompiled_header_not_found_highlighting=error +resharper_cpp_printf_bad_format_highlighting=warning +resharper_cpp_printf_extra_arg_highlighting=warning +resharper_cpp_printf_missed_arg_highlighting=error +resharper_cpp_printf_risky_format_highlighting=warning +resharper_cpp_private_special_member_function_is_not_implemented_highlighting=warning +resharper_cpp_range_based_for_incompatible_reference_highlighting=warning +resharper_cpp_redefinition_of_default_argument_in_override_function_highlighting=warning +resharper_cpp_redundant_access_specifier_highlighting=hint +resharper_cpp_redundant_base_class_access_specifier_highlighting=hint +resharper_cpp_redundant_blank_lines_highlighting=none +resharper_cpp_redundant_boolean_expression_argument_highlighting=warning +resharper_cpp_redundant_cast_expression_highlighting=hint +resharper_cpp_redundant_const_specifier_highlighting=hint +resharper_cpp_redundant_control_flow_jump_highlighting=hint +resharper_cpp_redundant_elaborated_type_specifier_highlighting=hint +resharper_cpp_redundant_else_keyword_highlighting=hint +resharper_cpp_redundant_else_keyword_inside_compound_statement_highlighting=hint +resharper_cpp_redundant_empty_declaration_highlighting=hint +resharper_cpp_redundant_empty_statement_highlighting=hint +resharper_cpp_redundant_explicit_template_arguments_highlighting=hint +resharper_cpp_redundant_inline_specifier_highlighting=hint +resharper_cpp_redundant_lambda_parameter_list_highlighting=hint +resharper_cpp_redundant_linebreak_highlighting=none +resharper_cpp_redundant_member_initializer_highlighting=suggestion +resharper_cpp_redundant_namespace_definition_highlighting=suggestion +resharper_cpp_redundant_parentheses_highlighting=hint +resharper_cpp_redundant_qualifier_highlighting=hint +resharper_cpp_redundant_space_highlighting=none +resharper_cpp_redundant_static_specifier_on_member_allocation_function_highlighting=hint +resharper_cpp_redundant_template_keyword_highlighting=warning +resharper_cpp_redundant_typename_keyword_highlighting=warning +resharper_cpp_redundant_void_argument_list_highlighting=suggestion +resharper_cpp_reinterpret_cast_from_void_ptr_highlighting=suggestion +resharper_cpp_remove_redundant_braces_highlighting=none +resharper_cpp_replace_memset_with_zero_initialization_highlighting=suggestion +resharper_cpp_replace_tie_with_structured_binding_highlighting=suggestion +resharper_cpp_return_no_value_in_non_void_function_highlighting=warning +resharper_cpp_smart_pointer_vs_make_function_highlighting=suggestion +resharper_cpp_some_object_members_might_not_be_initialized_highlighting=warning +resharper_cpp_special_function_without_noexcept_specification_highlighting=warning +resharper_cpp_static_data_member_in_unnamed_struct_highlighting=warning +resharper_cpp_static_specifier_on_anonymous_namespace_member_highlighting=suggestion +resharper_cpp_string_literal_to_char_pointer_conversion_highlighting=warning +resharper_cpp_syntax_warning_highlighting=warning +resharper_cpp_tabs_and_spaces_mismatch_highlighting=none +resharper_cpp_tabs_are_disallowed_highlighting=none +resharper_cpp_tabs_outside_indent_highlighting=none +resharper_cpp_template_parameter_shadowing_highlighting=warning +resharper_cpp_this_arg_member_func_delegate_ctor_is_unsuported_by_dot_net_core_highlighting=none +resharper_cpp_throw_expression_can_be_replaced_with_rethrow_highlighting=warning +resharper_cpp_too_wide_scope_highlighting=suggestion +resharper_cpp_too_wide_scope_init_statement_highlighting=hint +resharper_cpp_type_alias_never_used_highlighting=warning +resharper_cpp_ue4_blueprint_callable_function_may_be_const_highlighting=hint +resharper_cpp_ue4_blueprint_callable_function_may_be_static_highlighting=hint +resharper_cpp_ue4_coding_standard_naming_violation_warning_highlighting=hint +resharper_cpp_ue4_coding_standard_u_class_naming_violation_error_highlighting=error +resharper_cpp_ue4_probable_memory_issues_with_u_objects_in_container_highlighting=warning +resharper_cpp_ue4_probable_memory_issues_with_u_object_highlighting=warning +resharper_cpp_ue_blueprint_callable_function_unused_highlighting=warning +resharper_cpp_ue_blueprint_implementable_event_not_implemented_highlighting=warning +resharper_cpp_ue_incorrect_engine_directory_highlighting=error +resharper_cpp_ue_non_existent_input_action_highlighting=warning +resharper_cpp_ue_non_existent_input_axis_highlighting=warning +resharper_cpp_ue_source_file_without_predefined_macros_highlighting=warning +resharper_cpp_ue_source_file_without_standard_library_highlighting=error +resharper_cpp_ue_version_file_doesnt_exist_highlighting=error +resharper_cpp_uninitialized_dependent_base_class_highlighting=warning +resharper_cpp_uninitialized_non_static_data_member_highlighting=warning +resharper_cpp_union_member_of_reference_type_highlighting=warning +resharper_cpp_unnamed_namespace_in_header_file_highlighting=warning +resharper_cpp_unnecessary_whitespace_highlighting=none +resharper_cpp_unreachable_code_highlighting=warning +resharper_cpp_unsigned_zero_comparison_highlighting=warning +resharper_cpp_unused_include_directive_highlighting=warning +resharper_cpp_user_defined_literal_suffix_does_not_start_with_underscore_highlighting=warning +resharper_cpp_use_algorithm_with_count_highlighting=suggestion +resharper_cpp_use_associative_contains_highlighting=suggestion +resharper_cpp_use_auto_for_numeric_highlighting=hint +resharper_cpp_use_auto_highlighting=hint +resharper_cpp_use_elements_view_highlighting=suggestion +resharper_cpp_use_erase_algorithm_highlighting=suggestion +resharper_cpp_use_familiar_template_syntax_for_generic_lambdas_highlighting=suggestion +resharper_cpp_use_range_algorithm_highlighting=suggestion +resharper_cpp_use_std_size_highlighting=suggestion +resharper_cpp_use_structured_binding_highlighting=hint +resharper_cpp_use_type_trait_alias_highlighting=suggestion +resharper_cpp_using_result_of_assignment_as_condition_highlighting=warning +resharper_cpp_u_function_macro_call_has_no_effect_highlighting=warning +resharper_cpp_u_property_macro_call_has_no_effect_highlighting=warning +resharper_cpp_variable_can_be_made_constexpr_highlighting=suggestion +resharper_cpp_virtual_function_call_inside_ctor_highlighting=warning +resharper_cpp_virtual_function_in_final_class_highlighting=warning +resharper_cpp_volatile_parameter_in_declaration_highlighting=suggestion +resharper_cpp_wrong_includes_order_highlighting=hint +resharper_cpp_wrong_indent_size_highlighting=none +resharper_cpp_wrong_slashes_in_include_directive_highlighting=hint +resharper_cpp_zero_constant_can_be_replaced_with_nullptr_highlighting=suggestion +resharper_cpp_zero_valued_expression_used_as_null_pointer_highlighting=warning +resharper_create_specialized_overload_highlighting=hint +resharper_css_browser_compatibility_highlighting=warning +resharper_css_caniuse_feature_requires_prefix_highlighting=hint +resharper_css_caniuse_unsupported_feature_highlighting=hint +resharper_css_not_resolved_highlighting=error +resharper_css_obsolete_highlighting=hint +resharper_css_property_does_not_override_vendor_property_highlighting=warning +resharper_cyclic_reference_comment_highlighting=none +resharper_c_declaration_with_implicit_int_type_highlighting=warning +resharper_c_sharp_build_cs_invalid_module_name_highlighting=warning +resharper_c_sharp_missing_plugin_dependency_highlighting=warning +resharper_declaration_hides_highlighting=hint +resharper_declaration_is_empty_highlighting=warning +resharper_declaration_visibility_error_highlighting=error +resharper_default_value_attribute_for_optional_parameter_highlighting=warning +resharper_deleting_non_qualified_reference_highlighting=error +resharper_dl_tag_contains_non_dt_or_dd_elements_highlighting=hint +resharper_double_colons_expected_highlighting=error +resharper_double_colons_preferred_highlighting=suggestion +resharper_double_negation_in_pattern_highlighting=suggestion +resharper_double_negation_of_boolean_highlighting=warning +resharper_double_negation_operator_highlighting=suggestion +resharper_duplicate_identifier_error_highlighting=error +resharper_duplicate_reference_comment_highlighting=warning +resharper_duplicate_resource_highlighting=warning +resharper_duplicating_local_declaration_highlighting=warning +resharper_duplicating_parameter_declaration_error_highlighting=error +resharper_duplicating_property_declaration_error_highlighting=error +resharper_duplicating_property_declaration_highlighting=warning +resharper_duplicating_switch_label_highlighting=warning +resharper_dynamic_shift_right_op_is_not_int_highlighting=warning +resharper_elided_trailing_element_highlighting=warning +resharper_empty_constructor_highlighting=warning +resharper_empty_destructor_highlighting=warning +resharper_empty_embedded_statement_highlighting=warning +resharper_empty_for_statement_highlighting=warning +resharper_empty_general_catch_clause_highlighting=warning +resharper_empty_namespace_highlighting=warning +resharper_empty_object_property_declaration_highlighting=error +resharper_empty_return_value_for_type_annotated_function_highlighting=warning +resharper_empty_statement_highlighting=warning +resharper_empty_title_tag_highlighting=hint +resharper_enforce_do_while_statement_braces_highlighting=none +resharper_enforce_fixed_statement_braces_highlighting=none +resharper_enforce_foreach_statement_braces_highlighting=none +resharper_enforce_for_statement_braces_highlighting=none +resharper_enforce_if_statement_braces_highlighting=none +resharper_enforce_lock_statement_braces_highlighting=none +resharper_enforce_using_statement_braces_highlighting=none +resharper_enforce_while_statement_braces_highlighting=none +resharper_entity_name_captured_only_global_highlighting=warning +resharper_entity_name_captured_only_local_highlighting=warning +resharper_enumerable_sum_in_explicit_unchecked_context_highlighting=warning +resharper_enum_underlying_type_is_int_highlighting=warning +resharper_equal_expression_comparison_highlighting=warning +resharper_error_in_xml_doc_reference_highlighting=error +resharper_es6_feature_highlighting=error +resharper_es7_feature_highlighting=error +resharper_eval_arguments_name_error_highlighting=error +resharper_event_never_invoked_global_highlighting=suggestion +resharper_event_never_subscribed_to_global_highlighting=suggestion +resharper_event_never_subscribed_to_local_highlighting=suggestion +resharper_event_unsubscription_via_anonymous_delegate_highlighting=warning +resharper_experimental_feature_highlighting=error +resharper_explicit_caller_info_argument_highlighting=warning +resharper_expression_is_always_const_highlighting=warning +resharper_expression_is_always_null_highlighting=warning +resharper_field_can_be_made_read_only_global_highlighting=suggestion +resharper_field_can_be_made_read_only_local_highlighting=suggestion +resharper_field_hides_interface_property_with_default_implementation_highlighting=warning +resharper_foreach_can_be_converted_to_query_using_another_get_enumerator_highlighting=hint +resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_highlighting=hint +resharper_format_string_placeholders_mismatch_highlighting=warning +resharper_format_string_problem_highlighting=warning +resharper_for_can_be_converted_to_foreach_highlighting=suggestion +resharper_for_statement_condition_is_true_highlighting=warning +resharper_functions_used_before_declared_highlighting=none +resharper_function_complexity_overflow_highlighting=none +resharper_function_never_returns_highlighting=warning +resharper_function_parameter_named_arguments_highlighting=warning +resharper_function_recursive_on_all_paths_highlighting=warning +resharper_function_used_out_of_scope_highlighting=warning +resharper_gc_suppress_finalize_for_type_without_destructor_highlighting=warning +resharper_generic_enumerator_not_disposed_highlighting=warning +resharper_heuristically_unreachable_code_highlighting=warning +resharper_heuristic_unreachable_code_highlighting=warning +resharper_hex_color_value_with_alpha_highlighting=error +resharper_html_attributes_quotes_highlighting=hint +resharper_html_attribute_not_resolved_highlighting=warning +resharper_html_attribute_value_not_resolved_highlighting=warning +resharper_html_dead_code_highlighting=warning +resharper_html_event_not_resolved_highlighting=warning +resharper_html_id_duplication_highlighting=warning +resharper_html_id_not_resolved_highlighting=warning +resharper_html_obsolete_highlighting=warning +resharper_html_path_error_highlighting=warning +resharper_html_tag_not_closed_highlighting=error +resharper_html_tag_not_resolved_highlighting=warning +resharper_html_tag_should_be_self_closed_highlighting=warning +resharper_html_tag_should_not_be_self_closed_highlighting=warning +resharper_html_warning_highlighting=warning +resharper_identifier_typo_highlighting=suggestion +resharper_implicit_any_error_highlighting=error +resharper_implicit_any_type_warning_highlighting=warning +resharper_import_keyword_not_with_invocation_highlighting=error +resharper_inactive_preprocessor_branch_highlighting=warning +resharper_inconsistently_synchronized_field_highlighting=warning +resharper_inconsistent_function_returns_highlighting=warning +resharper_inconsistent_naming_highlighting=warning +resharper_inconsistent_order_of_locks_highlighting=warning +resharper_incorrect_blank_lines_near_braces_highlighting=none +resharper_incorrect_operand_in_type_of_comparison_highlighting=warning +resharper_incorrect_triple_slash_location_highlighting=warning +resharper_indexing_by_invalid_range_highlighting=warning +resharper_inheritdoc_consider_usage_highlighting=none +resharper_inheritdoc_invalid_usage_highlighting=warning +resharper_inline_out_variable_declaration_highlighting=suggestion +resharper_inline_temporary_variable_highlighting=hint +resharper_internal_module_highlighting=suggestion +resharper_internal_or_private_member_not_documented_highlighting=none +resharper_interpolated_string_expression_is_not_i_formattable_highlighting=warning +resharper_introduce_optional_parameters_global_highlighting=suggestion +resharper_introduce_optional_parameters_local_highlighting=suggestion +resharper_introduce_variable_to_apply_guard_highlighting=hint +resharper_int_division_by_zero_highlighting=warning +resharper_int_variable_overflow_highlighting=warning +resharper_int_variable_overflow_in_checked_context_highlighting=warning +resharper_int_variable_overflow_in_unchecked_context_highlighting=warning +resharper_invalid_attribute_value_highlighting=warning +resharper_invalid_json_syntax_highlighting=error +resharper_invalid_task_element_highlighting=none +resharper_invalid_value_highlighting=error +resharper_invalid_value_type_highlighting=warning +resharper_invalid_xml_doc_comment_highlighting=warning +resharper_invert_condition_1_highlighting=hint +resharper_invert_if_highlighting=hint +resharper_invocation_is_skipped_highlighting=hint +resharper_invocation_of_non_function_highlighting=warning +resharper_invoked_expression_maybe_non_function_highlighting=warning +resharper_invoke_as_extension_method_highlighting=suggestion +resharper_is_expression_always_false_highlighting=warning +resharper_is_expression_always_true_highlighting=warning +resharper_iterator_method_result_is_ignored_highlighting=warning +resharper_iterator_never_returns_highlighting=warning +resharper_join_declaration_and_initializer_highlighting=suggestion +resharper_join_declaration_and_initializer_js_highlighting=suggestion +resharper_join_null_check_with_usage_highlighting=suggestion +resharper_join_null_check_with_usage_when_possible_highlighting=none +resharper_json_validation_failed_highlighting=error +resharper_js_path_not_found_highlighting=error +resharper_js_unreachable_code_highlighting=warning +resharper_jump_must_be_in_loop_highlighting=warning +resharper_label_or_semicolon_expected_highlighting=error +resharper_lambda_expression_can_be_made_static_highlighting=none +resharper_lambda_expression_must_be_static_highlighting=suggestion +resharper_lambda_highlighting=suggestion +resharper_lambda_should_not_capture_context_highlighting=warning +resharper_less_specific_overload_than_main_signature_highlighting=warning +resharper_lexical_declaration_needs_block_highlighting=error +resharper_localizable_element_highlighting=warning +resharper_local_function_can_be_made_static_highlighting=none +resharper_local_function_hides_method_highlighting=warning +resharper_local_function_redefined_later_highlighting=warning +resharper_local_variable_hides_member_highlighting=warning +resharper_long_literal_ending_lower_l_highlighting=warning +resharper_loop_can_be_converted_to_query_highlighting=hint +resharper_loop_can_be_partly_converted_to_query_highlighting=none +resharper_loop_variable_is_never_changed_inside_loop_highlighting=warning +resharper_l_value_is_expected_highlighting=error +resharper_markup_attribute_typo_highlighting=suggestion +resharper_markup_text_typo_highlighting=suggestion +resharper_math_abs_method_is_redundant_highlighting=warning +resharper_math_clamp_min_greater_than_max_highlighting=warning +resharper_meaningless_default_parameter_value_highlighting=warning +resharper_member_can_be_internal_highlighting=none +resharper_member_can_be_made_static_global_highlighting=hint +resharper_member_can_be_made_static_local_highlighting=hint +resharper_member_can_be_private_global_highlighting=suggestion +resharper_member_can_be_private_local_highlighting=suggestion +resharper_member_can_be_protected_global_highlighting=suggestion +resharper_member_can_be_protected_local_highlighting=suggestion +resharper_member_hides_interface_member_with_default_implementation_highlighting=warning +resharper_member_hides_static_from_outer_class_highlighting=warning +resharper_member_initializer_value_ignored_highlighting=warning +resharper_merge_and_pattern_highlighting=suggestion +resharper_merge_cast_with_type_check_highlighting=suggestion +resharper_merge_conditional_expression_highlighting=suggestion +resharper_merge_conditional_expression_when_possible_highlighting=none +resharper_merge_into_logical_pattern_highlighting=hint +resharper_merge_into_negated_pattern_highlighting=hint +resharper_merge_into_pattern_highlighting=suggestion +resharper_merge_nested_property_patterns_highlighting=suggestion +resharper_merge_sequential_checks_highlighting=hint +resharper_merge_sequential_checks_when_possible_highlighting=none +resharper_method_has_async_overload_highlighting=suggestion +resharper_method_has_async_overload_with_cancellation_highlighting=suggestion +resharper_method_overload_with_optional_parameter_highlighting=warning +resharper_method_safe_this_highlighting=suggestion +resharper_method_supports_cancellation_highlighting=suggestion +resharper_missing_alt_attribute_in_img_tag_highlighting=hint +resharper_missing_attribute_highlighting=warning +resharper_missing_blank_lines_highlighting=none +resharper_missing_body_tag_highlighting=warning +resharper_missing_has_own_property_in_foreach_highlighting=warning +resharper_missing_head_and_body_tags_highlighting=warning +resharper_missing_head_tag_highlighting=warning +resharper_missing_indent_highlighting=none +resharper_missing_linebreak_highlighting=none +resharper_missing_space_highlighting=none +resharper_missing_title_tag_highlighting=hint +resharper_misuse_of_owner_function_this_highlighting=warning +resharper_more_specific_foreach_variable_type_available_highlighting=suggestion +resharper_more_specific_signature_after_less_specific_highlighting=warning +resharper_move_to_existing_positional_deconstruction_pattern_highlighting=hint +resharper_multiple_declarations_in_foreach_highlighting=error +resharper_multiple_nullable_attributes_usage_highlighting=warning +resharper_multiple_order_by_highlighting=warning +resharper_multiple_output_tags_highlighting=warning +resharper_multiple_resolve_candidates_in_text_highlighting=warning +resharper_multiple_spaces_highlighting=none +resharper_multiple_statements_on_one_line_highlighting=none +resharper_multiple_type_members_on_one_line_highlighting=none +resharper_must_use_return_value_highlighting=warning +resharper_mvc_action_not_resolved_highlighting=error +resharper_mvc_area_not_resolved_highlighting=error +resharper_mvc_controller_not_resolved_highlighting=error +resharper_mvc_invalid_model_type_highlighting=error +resharper_mvc_masterpage_not_resolved_highlighting=error +resharper_mvc_partial_view_not_resolved_highlighting=error +resharper_mvc_template_not_resolved_highlighting=error +resharper_mvc_view_component_not_resolved_highlighting=error +resharper_mvc_view_component_view_not_resolved_highlighting=error +resharper_mvc_view_not_resolved_highlighting=error +resharper_native_type_prototype_extending_highlighting=warning +resharper_native_type_prototype_overwriting_highlighting=warning +resharper_negation_of_relational_pattern_highlighting=suggestion +resharper_negative_equality_expression_highlighting=suggestion +resharper_negative_index_highlighting=warning +resharper_nested_string_interpolation_highlighting=suggestion +resharper_non_assigned_constant_highlighting=error +resharper_non_atomic_compound_operator_highlighting=warning +resharper_non_constant_equality_expression_has_constant_result_highlighting=warning +resharper_non_parsable_element_highlighting=warning +resharper_non_readonly_member_in_get_hash_code_highlighting=warning +resharper_non_volatile_field_in_double_check_locking_highlighting=warning +resharper_not_accessed_field_global_highlighting=suggestion +resharper_not_accessed_field_local_highlighting=warning +resharper_not_accessed_positional_property_global_highlighting=warning +resharper_not_accessed_positional_property_local_highlighting=warning +resharper_not_accessed_variable_highlighting=warning +resharper_not_all_paths_return_value_highlighting=warning +resharper_not_assigned_out_parameter_highlighting=warning +resharper_not_declared_in_parent_culture_highlighting=warning +resharper_not_null_member_is_not_initialized_highlighting=warning +resharper_not_observable_annotation_redundancy_highlighting=warning +resharper_not_overridden_in_specific_culture_highlighting=warning +resharper_not_resolved_highlighting=warning +resharper_not_resolved_in_text_highlighting=warning +resharper_nullable_warning_suppression_is_used_highlighting=none +resharper_n_unit_async_method_must_be_task_highlighting=warning +resharper_n_unit_attribute_produces_too_many_tests_highlighting=none +resharper_n_unit_auto_fixture_incorrect_argument_type_highlighting=warning +resharper_n_unit_auto_fixture_missed_test_attribute_highlighting=warning +resharper_n_unit_auto_fixture_missed_test_or_test_fixture_attribute_highlighting=warning +resharper_n_unit_auto_fixture_redundant_argument_in_inline_auto_data_attribute_highlighting=warning +resharper_n_unit_duplicate_values_highlighting=warning +resharper_n_unit_ignored_parameter_attribute_highlighting=warning +resharper_n_unit_implicit_unspecified_null_values_highlighting=warning +resharper_n_unit_incorrect_argument_type_highlighting=warning +resharper_n_unit_incorrect_expected_result_type_highlighting=warning +resharper_n_unit_incorrect_range_bounds_highlighting=warning +resharper_n_unit_method_with_parameters_and_test_attribute_highlighting=warning +resharper_n_unit_missing_arguments_in_test_case_attribute_highlighting=warning +resharper_n_unit_non_public_method_with_test_attribute_highlighting=warning +resharper_n_unit_no_values_provided_highlighting=warning +resharper_n_unit_parameter_type_is_not_compatible_with_attribute_highlighting=warning +resharper_n_unit_range_attribute_bounds_are_out_of_range_highlighting=warning +resharper_n_unit_range_step_sign_mismatch_highlighting=warning +resharper_n_unit_range_step_value_must_not_be_zero_highlighting=warning +resharper_n_unit_range_to_value_is_not_reachable_highlighting=warning +resharper_n_unit_redundant_argument_instead_of_expected_result_highlighting=warning +resharper_n_unit_redundant_argument_in_test_case_attribute_highlighting=warning +resharper_n_unit_redundant_expected_result_in_test_case_attribute_highlighting=warning +resharper_n_unit_test_case_attribute_requires_expected_result_highlighting=warning +resharper_n_unit_test_case_result_property_duplicates_expected_result_highlighting=warning +resharper_n_unit_test_case_result_property_is_obsolete_highlighting=warning +resharper_n_unit_test_case_source_cannot_be_resolved_highlighting=warning +resharper_n_unit_test_case_source_must_be_field_property_method_highlighting=warning +resharper_n_unit_test_case_source_must_be_static_highlighting=warning +resharper_n_unit_test_case_source_should_implement_i_enumerable_highlighting=warning +resharper_object_creation_as_statement_highlighting=warning +resharper_object_destructuring_without_parentheses_highlighting=error +resharper_object_literals_are_not_comma_free_highlighting=error +resharper_obsolete_element_error_highlighting=error +resharper_obsolete_element_highlighting=warning +resharper_octal_literals_not_allowed_error_highlighting=error +resharper_ol_tag_contains_non_li_elements_highlighting=hint +resharper_one_way_operation_contract_with_return_type_highlighting=warning +resharper_operation_contract_without_service_contract_highlighting=warning +resharper_operator_is_can_be_used_highlighting=warning +resharper_optional_parameter_hierarchy_mismatch_highlighting=warning +resharper_optional_parameter_ref_out_highlighting=warning +resharper_other_tags_inside_script1_highlighting=error +resharper_other_tags_inside_script2_highlighting=error +resharper_other_tags_inside_unclosed_script_highlighting=error +resharper_outdent_is_off_prev_level_highlighting=none +resharper_output_tag_required_highlighting=warning +resharper_out_parameter_value_is_always_discarded_global_highlighting=suggestion +resharper_out_parameter_value_is_always_discarded_local_highlighting=warning +resharper_overload_signature_inferring_highlighting=hint +resharper_overridden_with_empty_value_highlighting=warning +resharper_overridden_with_same_value_highlighting=suggestion +resharper_parameter_doesnt_make_any_sense_highlighting=warning +resharper_parameter_hides_member_highlighting=warning +resharper_parameter_only_used_for_precondition_check_global_highlighting=suggestion +resharper_parameter_only_used_for_precondition_check_local_highlighting=warning +resharper_parameter_type_can_be_enumerable_global_highlighting=hint +resharper_parameter_type_can_be_enumerable_local_highlighting=hint +resharper_parameter_value_is_not_used_highlighting=warning +resharper_partial_method_parameter_name_mismatch_highlighting=warning +resharper_partial_method_with_single_part_highlighting=warning +resharper_partial_type_with_single_part_highlighting=warning +resharper_pass_string_interpolation_highlighting=hint +resharper_path_not_resolved_highlighting=error +resharper_pattern_always_matches_highlighting=warning +resharper_pattern_is_always_true_or_false_highlighting=warning +resharper_pattern_never_matches_highlighting=warning +resharper_polymorphic_field_like_event_invocation_highlighting=warning +resharper_possible_infinite_inheritance_highlighting=warning +resharper_possible_intended_rethrow_highlighting=warning +resharper_possible_interface_member_ambiguity_highlighting=warning +resharper_possible_invalid_cast_exception_highlighting=warning +resharper_possible_invalid_cast_exception_in_foreach_loop_highlighting=warning +resharper_possible_invalid_operation_exception_highlighting=warning +resharper_possible_loss_of_fraction_highlighting=warning +resharper_possible_mistaken_argument_highlighting=warning +resharper_possible_mistaken_call_to_get_type_1_highlighting=warning +resharper_possible_mistaken_call_to_get_type_2_highlighting=warning +resharper_possible_multiple_enumeration_highlighting=warning +resharper_possible_multiple_write_access_in_double_check_locking_highlighting=warning +resharper_possible_null_reference_exception_highlighting=warning +resharper_possible_struct_member_modification_of_non_variable_struct_highlighting=warning +resharper_possible_unintended_linear_search_in_set_highlighting=warning +resharper_possible_unintended_queryable_as_enumerable_highlighting=suggestion +resharper_possible_unintended_reference_comparison_highlighting=warning +resharper_possible_write_to_me_highlighting=warning +resharper_possibly_impure_method_call_on_readonly_variable_highlighting=warning +resharper_possibly_incorrectly_broken_statement_highlighting=warning +resharper_possibly_missing_indexer_initializer_comma_highlighting=warning +resharper_possibly_mistaken_use_of_interpolated_string_insert_highlighting=warning +resharper_possibly_mistaken_use_of_params_method_highlighting=warning +resharper_possibly_unassigned_property_highlighting=hint +resharper_private_field_can_be_converted_to_local_variable_highlighting=warning +resharper_private_variable_can_be_made_readonly_highlighting=hint +resharper_property_can_be_made_init_only_global_highlighting=suggestion +resharper_property_can_be_made_init_only_local_highlighting=suggestion +resharper_property_getter_cannot_have_parameters_highlighting=error +resharper_property_not_resolved_highlighting=error +resharper_property_setter_must_have_single_parameter_highlighting=error +resharper_public_constructor_in_abstract_class_highlighting=suggestion +resharper_pure_attribute_on_void_method_highlighting=warning +resharper_qualified_expression_is_null_highlighting=warning +resharper_qualified_expression_maybe_null_highlighting=warning +resharper_razor_layout_not_resolved_highlighting=error +resharper_razor_section_not_resolved_highlighting=error +resharper_read_access_in_double_check_locking_highlighting=warning +resharper_redundant_abstract_modifier_highlighting=warning +resharper_redundant_always_match_subpattern_highlighting=suggestion +resharper_redundant_anonymous_type_property_name_highlighting=warning +resharper_redundant_argument_default_value_highlighting=warning +resharper_redundant_array_creation_expression_highlighting=hint +resharper_redundant_array_lower_bound_specification_highlighting=warning +resharper_redundant_assignment_highlighting=warning +resharper_redundant_attribute_parentheses_highlighting=hint +resharper_redundant_attribute_usage_property_highlighting=suggestion +resharper_redundant_base_constructor_call_highlighting=warning +resharper_redundant_base_qualifier_highlighting=warning +resharper_redundant_blank_lines_highlighting=none +resharper_redundant_block_highlighting=warning +resharper_redundant_bool_compare_highlighting=warning +resharper_redundant_case_label_highlighting=warning +resharper_redundant_cast_highlighting=warning +resharper_redundant_catch_clause_highlighting=warning +resharper_redundant_check_before_assignment_highlighting=warning +resharper_redundant_collection_initializer_element_braces_highlighting=hint +resharper_redundant_comparison_with_boolean_highlighting=warning +resharper_redundant_configure_await_highlighting=suggestion +resharper_redundant_css_hack_highlighting=warning +resharper_redundant_declaration_semicolon_highlighting=hint +resharper_redundant_default_member_initializer_highlighting=warning +resharper_redundant_delegate_creation_highlighting=warning +resharper_redundant_disable_warning_comment_highlighting=warning +resharper_redundant_discard_designation_highlighting=suggestion +resharper_redundant_else_block_highlighting=warning +resharper_redundant_empty_case_else_highlighting=warning +resharper_redundant_empty_constructor_highlighting=warning +resharper_redundant_empty_finally_block_highlighting=warning +resharper_redundant_empty_object_creation_argument_list_highlighting=hint +resharper_redundant_empty_object_or_collection_initializer_highlighting=warning +resharper_redundant_empty_switch_section_highlighting=warning +resharper_redundant_enumerable_cast_call_highlighting=warning +resharper_redundant_enum_case_label_for_default_section_highlighting=none +resharper_redundant_explicit_array_creation_highlighting=warning +resharper_redundant_explicit_array_size_highlighting=warning +resharper_redundant_explicit_nullable_creation_highlighting=warning +resharper_redundant_explicit_params_array_creation_highlighting=suggestion +resharper_redundant_explicit_positional_property_declaration_highlighting=warning +resharper_redundant_explicit_tuple_component_name_highlighting=warning +resharper_redundant_extends_list_entry_highlighting=warning +resharper_redundant_fixed_pointer_declaration_highlighting=suggestion +resharper_redundant_highlighting=warning +resharper_redundant_if_else_block_highlighting=hint +resharper_redundant_if_statement_then_keyword_highlighting=none +resharper_redundant_immediate_delegate_invocation_highlighting=suggestion +resharper_redundant_intermediate_variable_highlighting=hint +resharper_redundant_is_before_relational_pattern_highlighting=suggestion +resharper_redundant_iterator_keyword_highlighting=warning +resharper_redundant_jump_statement_highlighting=warning +resharper_redundant_lambda_parameter_type_highlighting=warning +resharper_redundant_lambda_signature_parentheses_highlighting=hint +resharper_redundant_linebreak_highlighting=none +resharper_redundant_local_class_name_highlighting=hint +resharper_redundant_local_function_name_highlighting=hint +resharper_redundant_logical_conditional_expression_operand_highlighting=warning +resharper_redundant_me_qualifier_highlighting=warning +resharper_redundant_my_base_qualifier_highlighting=warning +resharper_redundant_my_class_qualifier_highlighting=warning +resharper_redundant_name_qualifier_highlighting=warning +resharper_redundant_not_null_constraint_highlighting=warning +resharper_redundant_nullable_annotation_on_reference_type_constraint_highlighting=warning +resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_base_type_highlighting=warning +resharper_redundant_nullable_annotation_on_type_constraint_has_non_nullable_type_kind_highlighting=warning +resharper_redundant_nullable_flow_attribute_highlighting=warning +resharper_redundant_nullable_type_mark_highlighting=warning +resharper_redundant_nullness_attribute_with_nullable_reference_types_highlighting=warning +resharper_redundant_overflow_checking_context_highlighting=warning +resharper_redundant_overload_global_highlighting=suggestion +resharper_redundant_overload_local_highlighting=suggestion +resharper_redundant_overridden_member_highlighting=warning +resharper_redundant_params_highlighting=warning +resharper_redundant_parentheses_highlighting=none +resharper_redundant_parent_type_declaration_highlighting=warning +resharper_redundant_pattern_parentheses_highlighting=hint +resharper_redundant_property_parentheses_highlighting=hint +resharper_redundant_property_pattern_clause_highlighting=suggestion +resharper_redundant_qualifier_highlighting=warning +resharper_redundant_query_order_by_ascending_keyword_highlighting=hint +resharper_redundant_range_bound_highlighting=suggestion +resharper_redundant_readonly_modifier_highlighting=suggestion +resharper_redundant_record_body_highlighting=warning +resharper_redundant_record_class_keyword_highlighting=warning +resharper_redundant_setter_value_parameter_declaration_highlighting=hint +resharper_redundant_space_highlighting=none +resharper_redundant_string_format_call_highlighting=warning +resharper_redundant_string_interpolation_highlighting=suggestion +resharper_redundant_string_to_char_array_call_highlighting=warning +resharper_redundant_string_type_highlighting=suggestion +resharper_redundant_suppress_nullable_warning_expression_highlighting=warning +resharper_redundant_ternary_expression_highlighting=warning +resharper_redundant_to_string_call_for_value_type_highlighting=hint +resharper_redundant_to_string_call_highlighting=warning +resharper_redundant_type_arguments_of_method_highlighting=warning +resharper_redundant_type_cast_highlighting=warning +resharper_redundant_type_cast_structural_highlighting=warning +resharper_redundant_type_check_in_pattern_highlighting=warning +resharper_redundant_units_highlighting=warning +resharper_redundant_unsafe_context_highlighting=warning +resharper_redundant_using_directive_global_highlighting=warning +resharper_redundant_using_directive_highlighting=warning +resharper_redundant_variable_type_specification_highlighting=hint +resharper_redundant_verbatim_prefix_highlighting=suggestion +resharper_redundant_verbatim_string_prefix_highlighting=suggestion +resharper_redundant_with_expression_highlighting=suggestion +resharper_reference_equals_with_value_type_highlighting=warning +resharper_reg_exp_inspections_highlighting=warning +resharper_remove_constructor_invocation_highlighting=none +resharper_remove_redundant_braces_highlighting=none +resharper_remove_redundant_or_statement_false_highlighting=suggestion +resharper_remove_redundant_or_statement_true_highlighting=suggestion +resharper_remove_to_list_1_highlighting=suggestion +resharper_remove_to_list_2_highlighting=suggestion +resharper_replace_auto_property_with_computed_property_highlighting=hint +resharper_replace_indicing_with_array_destructuring_highlighting=hint +resharper_replace_indicing_with_short_hand_properties_after_destructuring_highlighting=hint +resharper_replace_object_pattern_with_var_pattern_highlighting=suggestion +resharper_replace_slice_with_range_indexer_highlighting=hint +resharper_replace_substring_with_range_indexer_highlighting=hint +resharper_replace_undefined_checking_series_with_object_destructuring_highlighting=hint +resharper_replace_with_destructuring_swap_highlighting=hint +resharper_replace_with_first_or_default_1_highlighting=suggestion +resharper_replace_with_first_or_default_2_highlighting=suggestion +resharper_replace_with_first_or_default_3_highlighting=suggestion +resharper_replace_with_first_or_default_4_highlighting=suggestion +resharper_replace_with_last_or_default_1_highlighting=suggestion +resharper_replace_with_last_or_default_2_highlighting=suggestion +resharper_replace_with_last_or_default_3_highlighting=suggestion +resharper_replace_with_last_or_default_4_highlighting=suggestion +resharper_replace_with_of_type_1_highlighting=suggestion +resharper_replace_with_of_type_2_highlighting=suggestion +resharper_replace_with_of_type_3_highlighting=suggestion +resharper_replace_with_of_type_any_1_highlighting=suggestion +resharper_replace_with_of_type_any_2_highlighting=suggestion +resharper_replace_with_of_type_count_1_highlighting=suggestion +resharper_replace_with_of_type_count_2_highlighting=suggestion +resharper_replace_with_of_type_first_1_highlighting=suggestion +resharper_replace_with_of_type_first_2_highlighting=suggestion +resharper_replace_with_of_type_first_or_default_1_highlighting=suggestion +resharper_replace_with_of_type_first_or_default_2_highlighting=suggestion +resharper_replace_with_of_type_last_1_highlighting=suggestion +resharper_replace_with_of_type_last_2_highlighting=suggestion +resharper_replace_with_of_type_last_or_default_1_highlighting=suggestion +resharper_replace_with_of_type_last_or_default_2_highlighting=suggestion +resharper_replace_with_of_type_long_count_highlighting=suggestion +resharper_replace_with_of_type_single_1_highlighting=suggestion +resharper_replace_with_of_type_single_2_highlighting=suggestion +resharper_replace_with_of_type_single_or_default_1_highlighting=suggestion +resharper_replace_with_of_type_single_or_default_2_highlighting=suggestion +resharper_replace_with_of_type_where_highlighting=suggestion +resharper_replace_with_simple_assignment_false_highlighting=suggestion +resharper_replace_with_simple_assignment_true_highlighting=suggestion +resharper_replace_with_single_assignment_false_highlighting=suggestion +resharper_replace_with_single_assignment_true_highlighting=suggestion +resharper_replace_with_single_call_to_any_highlighting=suggestion +resharper_replace_with_single_call_to_count_highlighting=suggestion +resharper_replace_with_single_call_to_first_highlighting=suggestion +resharper_replace_with_single_call_to_first_or_default_highlighting=suggestion +resharper_replace_with_single_call_to_last_highlighting=suggestion +resharper_replace_with_single_call_to_last_or_default_highlighting=suggestion +resharper_replace_with_single_call_to_single_highlighting=suggestion +resharper_replace_with_single_call_to_single_or_default_highlighting=suggestion +resharper_replace_with_single_or_default_1_highlighting=suggestion +resharper_replace_with_single_or_default_2_highlighting=suggestion +resharper_replace_with_single_or_default_3_highlighting=suggestion +resharper_replace_with_single_or_default_4_highlighting=suggestion +resharper_replace_with_string_is_null_or_empty_highlighting=suggestion +resharper_required_base_types_conflict_highlighting=warning +resharper_required_base_types_direct_conflict_highlighting=warning +resharper_required_base_types_is_not_inherited_highlighting=warning +resharper_requires_fallback_color_highlighting=warning +resharper_resource_item_not_resolved_highlighting=error +resharper_resource_not_resolved_highlighting=error +resharper_resx_not_resolved_highlighting=warning +resharper_return_from_global_scopet_with_value_highlighting=warning +resharper_return_type_can_be_enumerable_global_highlighting=hint +resharper_return_type_can_be_enumerable_local_highlighting=hint +resharper_return_type_can_be_not_nullable_highlighting=warning +resharper_return_value_of_pure_method_is_not_used_highlighting=warning +resharper_route_templates_action_route_prefix_can_be_extracted_to_controller_route_highlighting=hint +resharper_route_templates_ambiguous_matching_constraint_constructor_highlighting=warning +resharper_route_templates_ambiguous_route_match_highlighting=warning +resharper_route_templates_constraint_argument_cannot_be_converted_highlighting=warning +resharper_route_templates_controller_route_parameter_is_not_passed_to_methods_highlighting=hint +resharper_route_templates_duplicated_parameter_highlighting=warning +resharper_route_templates_matching_constraint_constructor_not_resolved_highlighting=warning +resharper_route_templates_method_missing_route_parameters_highlighting=hint +resharper_route_templates_optional_parameter_can_be_preceded_only_by_single_period_highlighting=warning +resharper_route_templates_optional_parameter_must_be_at_the_end_of_segment_highlighting=warning +resharper_route_templates_parameter_constraint_can_be_specified_highlighting=hint +resharper_route_templates_parameter_type_and_constraints_mismatch_highlighting=warning +resharper_route_templates_parameter_type_can_be_made_stricter_highlighting=suggestion +resharper_route_templates_route_parameter_constraint_not_resolved_highlighting=warning +resharper_route_templates_route_parameter_is_not_passed_to_method_highlighting=hint +resharper_route_templates_route_token_not_resolved_highlighting=warning +resharper_route_templates_symbol_not_resolved_highlighting=warning +resharper_route_templates_syntax_error_highlighting=warning +resharper_safe_cast_is_used_as_type_check_highlighting=suggestion +resharper_same_imports_with_different_name_highlighting=warning +resharper_same_variable_assignment_highlighting=warning +resharper_script_tag_has_both_src_and_content_attributes_highlighting=error +resharper_script_tag_with_content_before_includes_highlighting=hint +resharper_sealed_member_in_sealed_class_highlighting=warning +resharper_separate_control_transfer_statement_highlighting=none +resharper_service_contract_without_operations_highlighting=warning +resharper_shift_expression_real_shift_count_is_zero_highlighting=warning +resharper_shift_expression_result_equals_zero_highlighting=warning +resharper_shift_expression_right_operand_not_equal_real_count_highlighting=warning +resharper_shift_expression_zero_left_operand_highlighting=warning +resharper_similar_anonymous_type_nearby_highlighting=hint +resharper_similar_expressions_comparison_highlighting=warning +resharper_simplify_conditional_operator_highlighting=suggestion +resharper_simplify_conditional_ternary_expression_highlighting=suggestion +resharper_simplify_i_if_highlighting=suggestion +resharper_simplify_linq_expression_use_all_highlighting=suggestion +resharper_simplify_linq_expression_use_any_highlighting=suggestion +resharper_simplify_string_interpolation_highlighting=suggestion +resharper_specify_a_culture_in_string_conversion_explicitly_highlighting=warning +resharper_specify_string_comparison_highlighting=hint +resharper_specify_variable_type_explicitly_highlighting=hint +resharper_spin_lock_in_readonly_field_highlighting=warning +resharper_stack_alloc_inside_loop_highlighting=warning +resharper_statement_termination_highlighting=warning +resharper_static_member_initializer_referes_to_member_below_highlighting=warning +resharper_static_member_in_generic_type_highlighting=none +resharper_static_problem_in_text_highlighting=warning +resharper_string_compare_is_culture_specific_1_highlighting=warning +resharper_string_compare_is_culture_specific_2_highlighting=warning +resharper_string_compare_is_culture_specific_3_highlighting=warning +resharper_string_compare_is_culture_specific_4_highlighting=warning +resharper_string_compare_is_culture_specific_5_highlighting=warning +resharper_string_compare_is_culture_specific_6_highlighting=warning +resharper_string_compare_to_is_culture_specific_highlighting=warning +resharper_string_concatenation_to_template_string_highlighting=hint +resharper_string_ends_with_is_culture_specific_highlighting=none +resharper_string_index_of_is_culture_specific_1_highlighting=warning +resharper_string_index_of_is_culture_specific_2_highlighting=warning +resharper_string_index_of_is_culture_specific_3_highlighting=warning +resharper_string_last_index_of_is_culture_specific_1_highlighting=warning +resharper_string_last_index_of_is_culture_specific_2_highlighting=warning +resharper_string_last_index_of_is_culture_specific_3_highlighting=warning +resharper_string_literal_as_interpolation_argument_highlighting=suggestion +resharper_string_literal_typo_highlighting=suggestion +resharper_string_literal_wrong_quotes_highlighting=hint +resharper_string_starts_with_is_culture_specific_highlighting=none +resharper_structured_message_template_problem_highlighting=warning +resharper_struct_can_be_made_read_only_highlighting=suggestion +resharper_struct_member_can_be_made_read_only_highlighting=none +resharper_suggest_base_type_for_parameter_highlighting=hint +resharper_suggest_base_type_for_parameter_in_constructor_highlighting=hint +resharper_suggest_discard_declaration_var_style_highlighting=hint +resharper_suggest_var_or_type_built_in_types_highlighting=hint +resharper_suggest_var_or_type_deconstruction_declarations_highlighting=hint +resharper_suggest_var_or_type_elsewhere_highlighting=hint +resharper_suggest_var_or_type_simple_types_highlighting=hint +resharper_super_call_highlighting=suggestion +resharper_super_call_prohibits_this_highlighting=error +resharper_suppress_nullable_warning_expression_as_inverted_is_expression_highlighting=warning +resharper_suspicious_instanceof_check_highlighting=warning +resharper_suspicious_lambda_block_highlighting=warning +resharper_suspicious_lock_over_synchronization_primitive_highlighting=warning +resharper_suspicious_math_sign_method_highlighting=warning +resharper_suspicious_parameter_name_in_argument_null_exception_highlighting=warning +resharper_suspicious_this_usage_highlighting=warning +resharper_suspicious_typeof_check_highlighting=warning +resharper_suspicious_type_conversion_global_highlighting=warning +resharper_swap_via_deconstruction_highlighting=suggestion +resharper_switch_expression_handles_some_known_enum_values_with_exception_in_default_highlighting=hint +resharper_switch_statement_for_enum_misses_default_section_highlighting=hint +resharper_switch_statement_handles_some_known_enum_values_with_default_highlighting=hint +resharper_switch_statement_missing_some_enum_cases_no_default_highlighting=none +resharper_symbol_from_not_copied_locally_reference_used_warning_highlighting=warning +resharper_syntax_is_not_allowed_highlighting=warning +resharper_tabs_and_spaces_mismatch_highlighting=none +resharper_tabs_are_disallowed_highlighting=none +resharper_tabs_outside_indent_highlighting=none +resharper_tail_recursive_call_highlighting=hint +resharper_tasks_not_loaded_highlighting=warning +resharper_ternary_can_be_replaced_by_its_condition_highlighting=warning +resharper_this_in_global_context_highlighting=warning +resharper_thread_static_at_instance_field_highlighting=warning +resharper_thread_static_field_has_initializer_highlighting=warning +resharper_throw_must_be_followed_by_expression_highlighting=error +resharper_too_wide_local_variable_scope_highlighting=suggestion +resharper_try_cast_always_succeeds_highlighting=suggestion +resharper_try_statements_can_be_merged_highlighting=hint +resharper_ts_not_resolved_highlighting=error +resharper_ts_resolved_from_inaccessible_module_highlighting=error +resharper_type_guard_doesnt_affect_anything_highlighting=warning +resharper_type_guard_produces_never_type_highlighting=warning +resharper_type_parameter_can_be_variant_highlighting=suggestion +resharper_type_parameter_hides_type_param_from_outer_scope_highlighting=warning +resharper_ul_tag_contains_non_li_elements_highlighting=hint +resharper_unassigned_field_global_highlighting=suggestion +resharper_unassigned_field_local_highlighting=warning +resharper_unassigned_get_only_auto_property_highlighting=warning +resharper_unassigned_readonly_field_highlighting=warning +resharper_unclosed_script_highlighting=error +resharper_undeclared_global_variable_using_highlighting=warning +resharper_unexpected_value_highlighting=error +resharper_unknown_css_class_highlighting=warning +resharper_unknown_css_variable_highlighting=warning +resharper_unknown_css_vendor_extension_highlighting=hint +resharper_unknown_item_group_highlighting=warning +resharper_unknown_metadata_highlighting=warning +resharper_unknown_output_parameter_highlighting=warning +resharper_unknown_property_highlighting=warning +resharper_unknown_target_highlighting=warning +resharper_unknown_task_attribute_highlighting=warning +resharper_unknown_task_highlighting=warning +resharper_unnecessary_whitespace_highlighting=none +resharper_unreachable_switch_arm_due_to_integer_analysis_highlighting=warning +resharper_unreachable_switch_case_due_to_integer_analysis_highlighting=warning +resharper_unreal_header_tool_error_highlighting=error +resharper_unreal_header_tool_parser_error_highlighting=error +resharper_unreal_header_tool_warning_highlighting=warning +resharper_unsafe_comma_in_object_properties_list_highlighting=warning +resharper_unsupported_required_base_type_highlighting=warning +resharper_unused_anonymous_method_signature_highlighting=warning +resharper_unused_auto_property_accessor_global_highlighting=warning +resharper_unused_auto_property_accessor_local_highlighting=warning +resharper_unused_import_clause_highlighting=warning +resharper_unused_inherited_parameter_highlighting=hint +resharper_unused_locals_highlighting=warning +resharper_unused_local_function_highlighting=warning +resharper_unused_local_function_parameter_highlighting=warning +resharper_unused_local_function_return_value_highlighting=warning +resharper_unused_local_import_highlighting=warning +resharper_unused_member_global_highlighting=suggestion +resharper_unused_member_hierarchy_global_highlighting=suggestion +resharper_unused_member_hierarchy_local_highlighting=warning +resharper_unused_member_in_super_global_highlighting=suggestion +resharper_unused_member_in_super_local_highlighting=warning +resharper_unused_member_local_highlighting=warning +resharper_unused_method_return_value_global_highlighting=suggestion +resharper_unused_method_return_value_local_highlighting=warning +resharper_unused_parameter_global_highlighting=suggestion +resharper_unused_parameter_highlighting=warning +resharper_unused_parameter_in_partial_method_highlighting=warning +resharper_unused_parameter_local_highlighting=warning +resharper_unused_property_highlighting=warning +resharper_unused_tuple_component_in_return_value_highlighting=warning +resharper_unused_type_global_highlighting=suggestion +resharper_unused_type_local_highlighting=warning +resharper_unused_type_parameter_highlighting=warning +resharper_unused_variable_highlighting=warning +resharper_usage_of_definitely_unassigned_value_highlighting=warning +resharper_usage_of_possibly_unassigned_value_highlighting=warning +resharper_useless_binary_operation_highlighting=warning +resharper_useless_comparison_to_integral_constant_highlighting=warning +resharper_use_array_creation_expression_1_highlighting=suggestion +resharper_use_array_creation_expression_2_highlighting=suggestion +resharper_use_array_empty_method_highlighting=suggestion +resharper_use_as_instead_of_type_cast_highlighting=hint +resharper_use_await_using_highlighting=suggestion +resharper_use_cancellation_token_for_i_async_enumerable_highlighting=suggestion +resharper_use_collection_count_property_highlighting=suggestion +resharper_use_configure_await_false_for_async_disposable_highlighting=none +resharper_use_configure_await_false_highlighting=suggestion +resharper_use_deconstruction_highlighting=hint +resharper_use_deconstruction_on_parameter_highlighting=hint +resharper_use_empty_types_field_highlighting=suggestion +resharper_use_event_args_empty_field_highlighting=suggestion +resharper_use_format_specifier_in_format_string_highlighting=suggestion +resharper_use_implicitly_typed_variable_evident_highlighting=hint +resharper_use_implicitly_typed_variable_highlighting=none +resharper_use_implicit_by_val_modifier_highlighting=hint +resharper_use_indexed_property_highlighting=suggestion +resharper_use_index_from_end_expression_highlighting=suggestion +resharper_use_is_operator_1_highlighting=suggestion +resharper_use_is_operator_2_highlighting=suggestion +resharper_use_method_any_0_highlighting=suggestion +resharper_use_method_any_1_highlighting=suggestion +resharper_use_method_any_2_highlighting=suggestion +resharper_use_method_any_3_highlighting=suggestion +resharper_use_method_any_4_highlighting=suggestion +resharper_use_method_is_instance_of_type_highlighting=suggestion +resharper_use_nameof_expression_for_part_of_the_string_highlighting=none +resharper_use_nameof_expression_highlighting=suggestion +resharper_use_name_of_instead_of_type_of_highlighting=suggestion +resharper_use_negated_pattern_in_is_expression_highlighting=hint +resharper_use_negated_pattern_matching_highlighting=hint +resharper_use_nullable_annotation_instead_of_attribute_highlighting=suggestion +resharper_use_nullable_attributes_supported_by_compiler_highlighting=suggestion +resharper_use_nullable_reference_types_annotation_syntax_highlighting=warning +resharper_use_null_propagation_highlighting=hint +resharper_use_null_propagation_when_possible_highlighting=none +resharper_use_object_or_collection_initializer_highlighting=suggestion +resharper_use_of_implicit_global_in_function_scope_highlighting=warning +resharper_use_of_possibly_unassigned_property_highlighting=warning +resharper_use_pattern_matching_highlighting=suggestion +resharper_use_positional_deconstruction_pattern_highlighting=none +resharper_use_string_interpolation_highlighting=suggestion +resharper_use_switch_case_pattern_variable_highlighting=suggestion +resharper_use_throw_if_null_method_highlighting=none +resharper_use_verbatim_string_highlighting=hint +resharper_using_of_reserved_word_error_highlighting=error +resharper_using_of_reserved_word_highlighting=warning +resharper_value_parameter_not_used_highlighting=warning +resharper_value_range_attribute_violation_highlighting=warning +resharper_value_should_have_units_highlighting=error +resharper_variable_can_be_made_const_highlighting=hint +resharper_variable_can_be_made_let_highlighting=hint +resharper_variable_can_be_moved_to_inner_block_highlighting=hint +resharper_variable_can_be_not_nullable_highlighting=warning +resharper_variable_hides_outer_variable_highlighting=warning +resharper_variable_used_before_declared_highlighting=warning +resharper_variable_used_in_inner_scope_before_declared_highlighting=warning +resharper_variable_used_out_of_scope_highlighting=warning +resharper_vb_check_for_reference_equality_instead_1_highlighting=suggestion +resharper_vb_check_for_reference_equality_instead_2_highlighting=suggestion +resharper_vb_possible_mistaken_argument_highlighting=warning +resharper_vb_possible_mistaken_call_to_get_type_1_highlighting=warning +resharper_vb_possible_mistaken_call_to_get_type_2_highlighting=warning +resharper_vb_remove_to_list_1_highlighting=suggestion +resharper_vb_remove_to_list_2_highlighting=suggestion +resharper_vb_replace_with_first_or_default_highlighting=suggestion +resharper_vb_replace_with_last_or_default_highlighting=suggestion +resharper_vb_replace_with_of_type_1_highlighting=suggestion +resharper_vb_replace_with_of_type_2_highlighting=suggestion +resharper_vb_replace_with_of_type_any_1_highlighting=suggestion +resharper_vb_replace_with_of_type_any_2_highlighting=suggestion +resharper_vb_replace_with_of_type_count_1_highlighting=suggestion +resharper_vb_replace_with_of_type_count_2_highlighting=suggestion +resharper_vb_replace_with_of_type_first_1_highlighting=suggestion +resharper_vb_replace_with_of_type_first_2_highlighting=suggestion +resharper_vb_replace_with_of_type_first_or_default_1_highlighting=suggestion +resharper_vb_replace_with_of_type_first_or_default_2_highlighting=suggestion +resharper_vb_replace_with_of_type_last_1_highlighting=suggestion +resharper_vb_replace_with_of_type_last_2_highlighting=suggestion +resharper_vb_replace_with_of_type_last_or_default_1_highlighting=suggestion +resharper_vb_replace_with_of_type_last_or_default_2_highlighting=suggestion +resharper_vb_replace_with_of_type_single_1_highlighting=suggestion +resharper_vb_replace_with_of_type_single_2_highlighting=suggestion +resharper_vb_replace_with_of_type_single_or_default_1_highlighting=suggestion +resharper_vb_replace_with_of_type_single_or_default_2_highlighting=suggestion +resharper_vb_replace_with_of_type_where_highlighting=suggestion +resharper_vb_replace_with_single_assignment_1_highlighting=suggestion +resharper_vb_replace_with_single_assignment_2_highlighting=suggestion +resharper_vb_replace_with_single_call_to_any_highlighting=suggestion +resharper_vb_replace_with_single_call_to_count_highlighting=suggestion +resharper_vb_replace_with_single_call_to_first_highlighting=suggestion +resharper_vb_replace_with_single_call_to_first_or_default_highlighting=suggestion +resharper_vb_replace_with_single_call_to_last_highlighting=suggestion +resharper_vb_replace_with_single_call_to_last_or_default_highlighting=suggestion +resharper_vb_replace_with_single_call_to_single_highlighting=suggestion +resharper_vb_replace_with_single_call_to_single_or_default_highlighting=suggestion +resharper_vb_replace_with_single_or_default_highlighting=suggestion +resharper_vb_simplify_linq_expression_10_highlighting=hint +resharper_vb_simplify_linq_expression_1_highlighting=suggestion +resharper_vb_simplify_linq_expression_2_highlighting=suggestion +resharper_vb_simplify_linq_expression_3_highlighting=suggestion +resharper_vb_simplify_linq_expression_4_highlighting=suggestion +resharper_vb_simplify_linq_expression_5_highlighting=suggestion +resharper_vb_simplify_linq_expression_6_highlighting=suggestion +resharper_vb_simplify_linq_expression_7_highlighting=hint +resharper_vb_simplify_linq_expression_8_highlighting=hint +resharper_vb_simplify_linq_expression_9_highlighting=hint +resharper_vb_string_compare_is_culture_specific_1_highlighting=warning +resharper_vb_string_compare_is_culture_specific_2_highlighting=warning +resharper_vb_string_compare_is_culture_specific_3_highlighting=warning +resharper_vb_string_compare_is_culture_specific_4_highlighting=warning +resharper_vb_string_compare_is_culture_specific_5_highlighting=warning +resharper_vb_string_compare_is_culture_specific_6_highlighting=warning +resharper_vb_string_compare_to_is_culture_specific_highlighting=warning +resharper_vb_string_ends_with_is_culture_specific_highlighting=none +resharper_vb_string_index_of_is_culture_specific_1_highlighting=warning +resharper_vb_string_index_of_is_culture_specific_2_highlighting=warning +resharper_vb_string_index_of_is_culture_specific_3_highlighting=warning +resharper_vb_string_last_index_of_is_culture_specific_1_highlighting=warning +resharper_vb_string_last_index_of_is_culture_specific_2_highlighting=warning +resharper_vb_string_last_index_of_is_culture_specific_3_highlighting=warning +resharper_vb_string_starts_with_is_culture_specific_highlighting=none +resharper_vb_unreachable_code_highlighting=warning +resharper_vb_use_array_creation_expression_1_highlighting=suggestion +resharper_vb_use_array_creation_expression_2_highlighting=suggestion +resharper_vb_use_first_instead_highlighting=warning +resharper_vb_use_method_any_1_highlighting=suggestion +resharper_vb_use_method_any_2_highlighting=suggestion +resharper_vb_use_method_any_3_highlighting=suggestion +resharper_vb_use_method_any_4_highlighting=suggestion +resharper_vb_use_method_any_5_highlighting=suggestion +resharper_vb_use_method_is_instance_of_type_highlighting=suggestion +resharper_vb_use_type_of_is_operator_1_highlighting=suggestion +resharper_vb_use_type_of_is_operator_2_highlighting=suggestion +resharper_virtual_member_call_in_constructor_highlighting=warning +resharper_virtual_member_never_overridden_global_highlighting=suggestion +resharper_virtual_member_never_overridden_local_highlighting=suggestion +resharper_void_method_with_must_use_return_value_attribute_highlighting=warning +resharper_web_config_module_not_resolved_highlighting=error +resharper_web_config_module_qualification_resolve_highlighting=warning +resharper_web_config_redundant_add_namespace_tag_highlighting=warning +resharper_web_config_redundant_location_tag_highlighting=warning +resharper_web_config_tag_prefix_redundand_highlighting=warning +resharper_web_config_type_not_resolved_highlighting=error +resharper_web_config_unused_add_tag_highlighting=warning +resharper_web_config_unused_element_due_to_config_source_attribute_highlighting=warning +resharper_web_config_unused_remove_or_clear_tag_highlighting=warning +resharper_web_config_web_config_path_warning_highlighting=warning +resharper_web_config_wrong_module_highlighting=error +resharper_web_ignored_path_highlighting=none +resharper_web_mapped_path_highlighting=hint +resharper_with_expression_instead_of_initializer_highlighting=suggestion +resharper_with_statement_using_error_highlighting=error +resharper_wrong_expression_statement_highlighting=warning +resharper_wrong_indent_size_highlighting=none +resharper_wrong_metadata_use_highlighting=none +resharper_wrong_public_modifier_specification_highlighting=hint +resharper_wrong_require_relative_path_highlighting=hint +resharper_xaml_assign_null_to_not_null_attribute_highlighting=warning +resharper_xaml_avalonia_wrong_binding_mode_for_stream_binding_operator_highlighting=warning +resharper_xaml_binding_without_context_not_resolved_highlighting=hint +resharper_xaml_binding_with_context_not_resolved_highlighting=warning +resharper_xaml_compiled_binding_missing_data_type_error_highlighting_highlighting=error +resharper_xaml_constructor_warning_highlighting=warning +resharper_xaml_decimal_parsing_is_culture_dependent_highlighting=warning +resharper_xaml_dependency_property_resolve_error_highlighting=warning +resharper_xaml_duplicate_style_setter_highlighting=warning +resharper_xaml_dynamic_resource_error_highlighting=error +resharper_xaml_element_name_reference_not_resolved_highlighting=error +resharper_xaml_empty_grid_length_definition_highlighting=error +resharper_xaml_grid_definitions_can_be_converted_to_attribute_highlighting=hint +resharper_xaml_ignored_path_highlighting_highlighting=none +resharper_xaml_index_out_of_grid_definition_highlighting=warning +resharper_xaml_invalid_member_type_highlighting=error +resharper_xaml_invalid_resource_target_type_highlighting=error +resharper_xaml_invalid_resource_type_highlighting=error +resharper_xaml_invalid_type_highlighting=error +resharper_xaml_language_level_highlighting=error +resharper_xaml_mapped_path_highlighting_highlighting=hint +resharper_xaml_method_arguments_will_be_ignored_highlighting=warning +resharper_xaml_missing_grid_index_highlighting=warning +resharper_xaml_overloads_collision_highlighting=warning +resharper_xaml_parent_is_out_of_current_component_tree_highlighting=warning +resharper_xaml_path_error_highlighting=warning +resharper_xaml_possible_null_reference_exception_highlighting=suggestion +resharper_xaml_redundant_attached_property_highlighting=warning +resharper_xaml_redundant_binding_mode_attribute_highlighting=warning +resharper_xaml_redundant_collection_property_highlighting=warning +resharper_xaml_redundant_freeze_attribute_highlighting=warning +resharper_xaml_redundant_grid_definitions_highlighting=warning +resharper_xaml_redundant_grid_span_highlighting=warning +resharper_xaml_redundant_modifiers_attribute_highlighting=warning +resharper_xaml_redundant_namespace_alias_highlighting=warning +resharper_xaml_redundant_name_attribute_highlighting=warning +resharper_xaml_redundant_property_type_qualifier_highlighting=warning +resharper_xaml_redundant_resource_highlighting=warning +resharper_xaml_redundant_styled_value_highlighting=warning +resharper_xaml_redundant_update_source_trigger_attribute_highlighting=warning +resharper_xaml_redundant_xamarin_forms_class_declaration_highlighting=warning +resharper_xaml_resource_file_path_case_mismatch_highlighting=warning +resharper_xaml_routed_event_resolve_error_highlighting=warning +resharper_xaml_static_resource_not_resolved_highlighting=warning +resharper_xaml_style_class_not_found_highlighting=warning +resharper_xaml_style_invalid_target_type_highlighting=error +resharper_xaml_unexpected_text_token_highlighting=error +resharper_xaml_xaml_duplicate_device_family_type_view_highlighting_highlighting=error +resharper_xaml_xaml_mismatched_device_family_view_clr_name_highlighting_highlighting=warning +resharper_xaml_xaml_relative_source_default_mode_warning_highlighting_highlighting=warning +resharper_xaml_xaml_unknown_device_family_type_highlighting_highlighting=warning +resharper_xaml_xaml_xamarin_forms_data_type_and_binding_context_type_mismatched_highlighting_highlighting=warning +resharper_xaml_x_key_attribute_disallowed_highlighting=error +resharper_xml_doc_comment_syntax_problem_highlighting=warning +resharper_xunit_xunit_test_with_console_output_highlighting=warning + +[*.{cshtml,htm,html,proto,razor}] +indent_style=tab +indent_size=tab +tab_width=4 + +[*.{asax,ascx,aspx,axaml,c,c++,cc,cginc,compute,cp,cpp,cs,css,cu,cuh,cxx,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,js,jsx,master,mpp,mq4,mq5,mqh,paml,skin,tpp,ts,tsx,usf,ush,vb,xaml,xamlx,xoml}] +indent_style=space +indent_size=4 +tab_width=4 + +[ "*.proto" ] +indent_style=tab +indent_size=tab +tab_width=4 + +[*.{asax,ascx,aspx,axaml,cs,cshtml,css,htm,html,js,jsx,master,paml,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}] +indent_style=space +indent_size=4 +tab_width=4 + +[*.{appxmanifest,axml,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] +indent_style=space +indent_size=2 +tab_width=2 + +[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}] +indent_style=space +indent_size= 4 +tab_width= 4 +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +insert_final_newline = true diff --git a/Penumbra.Api/.gitignore b/Penumbra.Api/.gitignore new file mode 100644 index 0000000..3e16852 --- /dev/null +++ b/Penumbra.Api/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +.vs/ \ No newline at end of file diff --git a/Penumbra.Api/Api/Collection.cs b/Penumbra.Api/Api/Collection.cs new file mode 100644 index 0000000..083bbea --- /dev/null +++ b/Penumbra.Api/Api/Collection.cs @@ -0,0 +1,66 @@ +using Penumbra.Api.Enums; + +namespace Penumbra.Api.Api; + +/// API methods pertaining to collection management. +public interface IPenumbraApiCollection +{ + /// A list of the GUIDs of all currently installed collections together with their display names, excluding the empty collection. + public Dictionary GetCollections(); + + /// Returns all collections for which either + /// + /// the name is equal to the given identifier up to case, + /// the identifier is parsable to a GUID and the GUID corresponds to an existing collection, + /// or the identifier is at least 8 characters long and the GUID as a hex-string starts with the identifier. + /// + /// + public List<(Guid Id, string Name)> GetCollectionsByIdentifier(string identifier); + + /// A dictionary of affected items in via GUID and known objects or null. + public Dictionary GetChangedItemsForCollection(Guid collectionId); + + /// The GUID and name of the collection assigned to the given , the empty GUID for the empty collection, or null if nothing is assigned. + public (Guid Id, string Name)? GetCollection(ApiCollectionType type); + + /// Return whether the object at produces a valid identifier, if the identifier has a collection assigned, and the collection that affects the object. + public (bool ObjectValid, bool IndividualSet, (Guid Id, string Name) EffectiveCollection) GetCollectionForObject(int gameObjectIdx); + + /// + /// Set a collection by GUID for a specific type. + /// + /// The collection type to set. + /// The GUID of the collection to set it to, null to remove the association if allowed. + /// Allow only setting existing types or also creating an unset type. + /// Allow deleting existing collections if is empty. + /// InvalidArgument if type is invalid, + /// NothingChanged if the new collection is the same as the old,
+ /// AssignmentDeletionDisallowed if is null and is false, and the assignment exists,
+ /// or if Default, Current or Interface would be deleted.
+ /// CollectionMissing if the new collection can not be found,
+ /// AssignmentCreationDisallowed if is false and the assignment does not exist,
+ /// or Success, as well as the GUID of the previous collection (empty if no assignment existed). + ///
+ public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollection(ApiCollectionType type, Guid? collectionId, bool allowCreateNew, + bool allowDelete); + + /// + /// Set a collection by GUID for a specific game object. + /// + /// The index of the desired game object in the object table. + /// The GUID of the collection to set it to, null to remove the association if allowed. + /// Allow only setting existing individuals or also creating a new individual assignment. + /// Allow deleting existing individual assignments if is null. + /// InvalidIdentifier if does not produce an existing game object or the object is not identifiable, + /// NothingChanged if the new collection is the same as the old,
+ /// AssignmentDeletionDisallowed if is null and is false, and the assignment exists,
+ /// CollectionMissing if the new collection can not be found,
+ /// AssignmentCreationDisallowed if is false and the assignment does not exist,
+ /// or Success, as well as the name of the previous collection (empty if no assignment existed).
+ public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollectionForObject(int gameObjectIdx, Guid? collectionId, bool allowCreateNew, + bool allowDelete); + + /// Obtain a function object that can check if the current collection contains a given changed item by listing the mods changing it. + /// Throws an on invocation if the collection storage is not valid anymore, so clear this on . + public Func CheckCurrentChangedItemFunc(); +} diff --git a/Penumbra.Api/Api/Editing.cs b/Penumbra.Api/Api/Editing.cs new file mode 100644 index 0000000..fa5b096 --- /dev/null +++ b/Penumbra.Api/Api/Editing.cs @@ -0,0 +1,28 @@ +using Penumbra.Api.Enums; + +namespace Penumbra.Api.Api; + +/// API methods pertaining to the editing of mods or game files. +public interface IPenumbraApiEditing +{ + /// + /// Convert the given texture file into a different type or format and potentially add mip maps. + /// + /// The path to the input file, which may be of .dds, .tex or .png format. + /// The desired output path. Can be the same as input. + /// The file type and format to convert the data to. + /// Whether to add mip maps or not. Ignored for .png. + /// A task for when the conversion is finished or has failed. + public Task ConvertTextureFile(string inputFile, string outputFile, TextureType textureType, bool mipMaps); + + /// + /// Convert the given RGBA32 texture data into a different type or format and potentially add mip maps. + /// + /// The input byte data for a picture given in RGBA32 format. + /// The width of the input picture. The height is computed from the size of and this. + /// The desired output path. Can be the same as input. + /// The file type and format to convert the data to. + /// Whether to add mip maps or not. Ignored for .png. + /// A task for when the conversion is finished or has failed. + public Task ConvertTextureData(byte[] rgbaData, int width, string outputFile, TextureType textureType, bool mipMaps); +} diff --git a/Penumbra.Api/Api/GameState.cs b/Penumbra.Api/Api/GameState.cs new file mode 100644 index 0000000..8367f7d --- /dev/null +++ b/Penumbra.Api/Api/GameState.cs @@ -0,0 +1,52 @@ +using Penumbra.Api.Enums; + +namespace Penumbra.Api.Api; + +/// API methods pertaining to the currently tracked game state. +public interface IPenumbraApiGameState +{ + /// + /// The game object associated with the given draw object + /// and the GUID and name of the collection associated with this game object. + public (nint GameObject, (Guid Id, string Name) Collection) GetDrawObjectInfo(nint drawObject); + + /// + /// Obtain the parent game object index for an unnamed cutscene actor by its index. + /// + /// + /// The parent game object index. + public int GetCutsceneParentIndex(int actorIdx); + + /// + /// Set the cutscene parent of in Penumbras internal state to a new value. + /// + /// The index of the cutscene actor to be changed. + /// The new index of the cutscene actors parent or -1 for no parent. + /// Success when the new parent could be set, or InvalidArgument if either index is out of its respective range. + /// + /// Checks that the new parent exists as a game object if the value is not -1 before assigning. If it does not, InvalidArgument is given, too. + /// Please only use this for good reason and if you know what you are doing, probably only for actor copies you actually create yourself. + /// + public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx); + + /// + /// Triggered when a character base is created and a corresponding gameObject could be found, + /// before the Draw Object is actually created, so customize and equipdata can be manipulated beforehand. + /// + /// + public event CreatingCharacterBaseDelegate? CreatingCharacterBase; + + /// + /// Triggered after a character base was created if a corresponding gameObject could be found, + /// so you can apply flag changes after finishing. + /// + /// + public event CreatedCharacterBaseDelegate? CreatedCharacterBase; + + /// + /// Triggered whenever a resource is redirected by Penumbra for a specific, identified game object. + /// Does not trigger if the resource is not requested for a known game object. + /// + /// + public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; +} diff --git a/Penumbra.Api/Api/IPenumbraApi.cs b/Penumbra.Api/Api/IPenumbraApi.cs new file mode 100644 index 0000000..1cd5407 --- /dev/null +++ b/Penumbra.Api/Api/IPenumbraApi.cs @@ -0,0 +1,41 @@ +namespace Penumbra.Api.Api; + +/// The entire API. +public interface IPenumbraApi : IPenumbraApiBase +{ + /// + public IPenumbraApiCollection Collection { get; } + + /// + public IPenumbraApiEditing Editing { get; } + + /// + public IPenumbraApiGameState GameState { get; } + + /// + public IPenumbraApiMeta Meta { get; } + + /// + public IPenumbraApiMods Mods { get; } + + /// + public IPenumbraApiModSettings ModSettings { get; } + + /// + public IPenumbraApiPluginState PluginState { get; } + + /// + public IPenumbraApiRedraw Redraw { get; } + + /// + public IPenumbraApiResolve Resolve { get; } + + /// + public IPenumbraApiResourceTree ResourceTree { get; } + + /// + public IPenumbraApiTemporary Temporary { get; } + + /// + public IPenumbraApiUi Ui { get; } +} diff --git a/Penumbra.Api/Api/IPenumbraApiBase.cs b/Penumbra.Api/Api/IPenumbraApiBase.cs new file mode 100644 index 0000000..0a32ea5 --- /dev/null +++ b/Penumbra.Api/Api/IPenumbraApiBase.cs @@ -0,0 +1,16 @@ +namespace Penumbra.Api.Api; + +/// Base interface for the API that is always available, regardless of version. +public interface IPenumbraApiBase +{ + /// + /// The API version is staggered in two parts. + /// The major/Breaking version only increments if there are changes breaking backwards compatibility. + /// The minor/Feature version increments any time there is something added + /// and resets when Breaking is incremented. + /// + public (int Breaking, int Feature) ApiVersion { get; } + + /// Whether the API is still usable. + public bool Valid { get; } +} diff --git a/Penumbra.Api/Api/Meta.cs b/Penumbra.Api/Api/Meta.cs new file mode 100644 index 0000000..6a3e987 --- /dev/null +++ b/Penumbra.Api/Api/Meta.cs @@ -0,0 +1,13 @@ +namespace Penumbra.Api.Api; + +/// API methods pertaining to current metadata manipulations. +public interface IPenumbraApiMeta +{ + /// A base64 encoded, zipped json-string with a prepended version-byte of the current manipulations + /// in the collection currently associated with the player. + public string GetPlayerMetaManipulations(); + + /// A base64 encoded, zipped json-string with a prepended version-byte of the current manipulations + /// in the given collection applying to the given game object or the default collection if it does not exist. + public string GetMetaManipulations(int gameObjectIdx); +} diff --git a/Penumbra.Api/Api/ModSettings.cs b/Penumbra.Api/Api/ModSettings.cs new file mode 100644 index 0000000..2a1acea --- /dev/null +++ b/Penumbra.Api/Api/ModSettings.cs @@ -0,0 +1,77 @@ +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.Api; + +/// API methods pertaining to the management of mod settings. +public interface IPenumbraApiModSettings +{ + /// + /// Obtain the potential settings of a mod given by its name or . + /// + /// A dictionary of group names to lists of option names and the group type. Null if the mod could not be found. + public AvailableModSettings? GetAvailableModSettings(string modDirectory, string modName); + + /// + /// Obtain the enabled state, the priority, the settings of a mod given by its name or in the specified collection. + /// + /// Specify the collection. + /// Specify the mod via its directory name. + /// Specify the mod via its (non-unique) display name. + /// Whether the settings need to be from the given collection or can be inherited from any other by it. (True: given collection only) + /// Whether the settings need to be actual settings or can be temporary. + /// The key for the settings lock. If is false, settings with a key greater than 0 that is different from this will be ignored. + /// ModMissing, CollectionMissing or Success. + /// On Success, a tuple of Enabled State, Priority, a dictionary of option group names and lists of enabled option names and a bool whether the settings are inherited (true) or not. + public (PenumbraApiEc, (bool, int, Dictionary>, bool, bool)?) GetCurrentModSettingsWithTemp(Guid collectionId, + string modDirectory, string modName, bool ignoreInheritance, bool ignoreTemporary, int key); + + /// + public (PenumbraApiEc, (bool, int, Dictionary>, bool)?) GetCurrentModSettings(Guid collectionId, + string modDirectory, string modName, bool ignoreInheritance); + + /// Obtain the enabled state, the priority, the settings of all mods in the specified collection. + /// Specify the collection. + /// Whether the settings need to be from the given collection or can be inherited from any other by it. (True: given collection only) + /// Whether the settings need to be actual settings or can be temporary. + /// The key for the settings lock. If is false, settings with a key greater than 0 that is different from this will be ignored. + /// CollectionMissing or Success, on Success, a dictionary of mod directory names to a tuple of (Enabled, Priority, Settings, Inherited, Temporary). Mods that have no settings at all are left out. + public (PenumbraApiEc, Dictionary>, bool, bool)>?) GetAllModSettings(Guid collectionId, + bool ignoreInheritance, bool ignoreTemporary, int key); + + /// Try to set the inheritance state of a mod in a collection. + /// ModMissing, CollectionMissing, InvalidArgument (GUID is nil), NothingChanged or Success. + public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit); + + /// Try to set the enabled state of a mod in a collection. + /// ModMissing, CollectionMissing, InvalidArgument (GUID is nil), NothingChanged or Success. + public PenumbraApiEc TrySetMod(Guid collectionId, string modDirectory, string modName, bool enabled); + + /// Try to set the priority of a mod in a collection. + /// ModMissing, CollectionMissing, InvalidArgument (GUID is nil), NothingChanged or Success. + public PenumbraApiEc TrySetModPriority(Guid collectionId, string modDirectory, string modName, int priority); + + /// Try to set a specific option group of a mod in the given collection to a specific value. + /// Removes inheritance. Single Selection groups should provide a single option, Multi Selection can provide multiple. + /// If any setting can not be found, it will not change anything. + /// ModMissing, CollectionMissing, OptionGroupMissing, SettingMissing, InvalidArgument (GUID is nil), NothingChanged or Success. + public PenumbraApiEc TrySetModSetting(Guid collectionId, string modDirectory, string modName, string optionGroupName, string optionName); + + /// + public PenumbraApiEc TrySetModSettings(Guid collectionId, string modDirectory, string modName, string optionGroupName, + IReadOnlyList optionNames); + + /// This event gets fired when any setting in any collection changes. + /// + public event ModSettingChangedDelegate? ModSettingChanged; + + /// + /// Copy all current settings for a mod to another mod. + /// + /// Specify the collection to work in, leave null to do it in all collections. + /// Specify the mod to take the settings from via its directory name. + /// Specify the mod to put the settings on via its directory name. If the mod does not exist, it will be added as unused settings. + /// CollectionMissing if collectionName is not empty but does not exist or Success. + /// If the target mod exists, the settings will be fixed before being applied. If the source mod does not exist, it will use unused settings if available and remove existing settings otherwise. + public PenumbraApiEc CopyModSettings(Guid? collectionId, string modDirectoryFrom, string modDirectoryTo); +} diff --git a/Penumbra.Api/Api/Mods.cs b/Penumbra.Api/Api/Mods.cs new file mode 100644 index 0000000..a2164d8 --- /dev/null +++ b/Penumbra.Api/Api/Mods.cs @@ -0,0 +1,78 @@ +using Penumbra.Api.Enums; + +namespace Penumbra.Api.Api; + +/// API methods pertaining to management of mods. +public interface IPenumbraApiMods +{ + /// A list of all installed mods. The first string is their directory name, the second string is their mod name. + public Dictionary GetModList(); + + /// Try to unpack and install a valid mod file (.pmp, .ttmp, .ttmp2) as if installed manually. + /// The file that should be unpacked. + /// Success, MissingFile. Success does not indicate successful installing, just successful queueing for install. + public PenumbraApiEc InstallMod(string modFilePackagePath); + + /// Try to reload an existing mod given by its name or . + /// Reload is the same as if triggered by button press and might delete the mod if it is not valid anymore. + /// ModMissing if the mod can not be found or Success + public PenumbraApiEc ReloadMod(string modDirectory, string modName); + + /// Try to add a new mod inside the mod root directory. + /// Note that success does only imply a successful call, not a successful mod load. + /// The name (not full name) of the mod directory. + /// FileMissing if does not exist, InvalidArgument if the path leads outside the root directory, Success otherwise. + public PenumbraApiEc AddMod(string modDirectory); + + /// Try to delete a mod given by its name or . + /// Note that success does only imply a successful call, not successful deletion. + /// NothingDone if the mod can not be found, Success otherwise. + public PenumbraApiEc DeleteMod(string modDirectory, string modName); + + /// Triggers whenever a mod is deleted. + /// The base directory name of the deleted mod. + public event Action? ModDeleted; + + /// Triggers whenever a mod is deleted. + /// The base directory name of the new mod. + public event Action? ModAdded; + + /// Triggers whenever a mods base name is changed from inside Penumbra. + /// The previous base directory name of the mod and the new base directory name of the mod. + public event Action? ModMoved; + + /// + /// Get the internal full filesystem path including search order for the specified mod + /// given by its name or . + /// + /// On Success, the full path, a bool indicating whether the entire path is default (true) or manually set (false), + /// and a bool indicating whether the sort order name ignoring the folder path is default (true) or manually set (false). + /// Otherwise, returns ModMissing if the mod can not be found. + public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName); + + /// + /// Set the internal search order and filesystem path of the specified mod + /// given by its name or + /// to the . + /// + /// InvalidArgument if is empty, ModMissing if the mod can not be found, + /// PathRenameFailed if could not be set or Success. + public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath); + + /// Get the overall changed items of a single mod given by its name or , regardless of settings. + /// A possibly empty dictionary of affected items and known objects or null. + public Dictionary GetChangedItems(string modDirectory, string modName); + + /// Get a dictionary of dictionaries to check all mods changed items. + /// A dictionary of mod identifier to changed item dictionary. + /// Throws an on access if the mod storage is not valid anymore, so clear this on . + public IReadOnlyDictionary> GetChangedItemAdapterDictionary(); + + /// Get a list of dictionaries to check all mods changed items. + /// A list all mods changed item dictionaries. + /// + /// The order of mods is unspecified, but the same as in GetModList (assuming no changes in mods have taken place between calls).
+ /// Throws an on access if the mod storage is not valid anymore, so clear this on . + ///
+ public IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)> GetChangedItemAdapterList(); +} diff --git a/Penumbra.Api/Api/PluginState.cs b/Penumbra.Api/Api/PluginState.cs new file mode 100644 index 0000000..033e2c9 --- /dev/null +++ b/Penumbra.Api/Api/PluginState.cs @@ -0,0 +1,26 @@ +namespace Penumbra.Api.Api; + +/// API methods pertaining to Penumbras own state. +public interface IPenumbraApiPluginState +{ + /// The full path of the current penumbra root directory. + public string GetModDirectory(); + + /// The entire current penumbra configuration as a json encoded string. + public string GetConfiguration(); + + /// + /// Fired whenever a mod directory change is finished. + /// + /// The full path of the mod directory and whether Penumbra treats it as valid. + public event Action? ModDirectoryChanged; + + /// True if Penumbra is enabled, false otherwise. + public bool GetEnabledState(); + + /// + /// Fired whenever the enabled state of Penumbra changes. + /// + /// True if the new state is enabled, false if the new state is disabled + public event Action? EnabledChange; +} diff --git a/Penumbra.Api/Api/Redraw.cs b/Penumbra.Api/Api/Redraw.cs new file mode 100644 index 0000000..3c4854d --- /dev/null +++ b/Penumbra.Api/Api/Redraw.cs @@ -0,0 +1,23 @@ +using Penumbra.Api.Enums; + +namespace Penumbra.Api.Api; + +/// API methods pertaining to the redrawing of actors. +public interface IPenumbraApiRedraw +{ + /// + /// Queue redrawing of the actor with the given object , if it exists, with the given RedrawType . + /// + public void RedrawObject(int gameObjectIndex, RedrawType setting); + + /// + /// Queue redrawing of all currently available actors with the given RedrawType . + /// + public void RedrawAll(RedrawType setting); + + /// + /// Triggered whenever a game object is redrawn via Penumbra. + /// + /// / + public event GameObjectRedrawnDelegate? GameObjectRedrawn; +} diff --git a/Penumbra.Api/Api/Resolve.cs b/Penumbra.Api/Api/Resolve.cs new file mode 100644 index 0000000..c80fbcb --- /dev/null +++ b/Penumbra.Api/Api/Resolve.cs @@ -0,0 +1,64 @@ +using Lumina.Data; + +namespace Penumbra.Api.Api; + +/// API methods pertaining to the resolving of paths. +public interface IPenumbraApiResolve +{ + /// + /// Resolve a given via Penumbra using the Base collection. + /// + /// The resolved path, or the given path if Penumbra would not manipulate it. + public string ResolveDefaultPath(string gamePath); + + /// + /// Resolve a given via Penumbra using the Interface collection. + /// + /// The resolved path, or the given path if Penumbra would not manipulate it. + public string ResolveInterfacePath(string gamePath); + + /// + /// Resolve a given via Penumbra using collection applying to the + /// given by its index in the game object table. + /// + /// If the object does not exist in the table, the default collection is used. + /// The resolved path, or the given path if Penumbra would not manipulate it. + public string ResolveGameObjectPath(string gamePath, int gameObjectIdx); + + /// + /// Resolve a given via Penumbra using the collection currently applying to the player character. + /// + /// The resolved path, or the given path if Penumbra would not manipulate it. + public string ResolvePlayerPath(string gamePath); + + /// + /// Reverse resolves a given local into its replacement in form of all applicable game paths + /// for the collection applying to the th game object in the game object table. + /// + /// If the object does not exist in the table, the default collection is used. + /// A list of game paths resolving to the modded path. + public string[] ReverseResolveGameObjectPath(string moddedPath, int gameObjectIdx); + + /// + /// Reverse resolves a given local into its replacement in form of all applicable game paths + /// for the collection currently applying to the player character. + /// + /// A list of game paths resolving to the modded path. + public string[] ReverseResolvePlayerPath(string moddedPath); + + /// + /// Resolve all game paths in and reserve all paths in at once. + /// + /// Paths to forward-resolve. + /// Paths to reverse-resolve. + /// A pair of an array of forward-resolved single paths of the same length as and an array of arrays of reverse-resolved paths. + /// The outer array has the same length as while each inner array can have arbitrary length. + public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse); + + /// + /// Resolve all game paths in and reserve all paths in at once asynchronously. + /// + /// + /// Can be called from outside of framework. Can theoretically produce incoherent state when collections change during evaluation. + public Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse); +} diff --git a/Penumbra.Api/Api/ResourceTree.cs b/Penumbra.Api/Api/ResourceTree.cs new file mode 100644 index 0000000..18ef173 --- /dev/null +++ b/Penumbra.Api/Api/ResourceTree.cs @@ -0,0 +1,73 @@ +using Newtonsoft.Json.Linq; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.Api; + +/// API methods pertaining to the tracking of resources in use by actors. +public interface IPenumbraApiResourceTree +{ + /// + /// Get the given game objects' resources, as dictionaries of actual paths (that may be FS paths for redirected resources, or game paths for swapped or vanilla resources) to game paths. + /// + /// The game object indices for which to get the resources. + /// An array of resource path dictionaries, of the same length and in the same order as the given game object index array. + /// This function is best called right after the game objects are redrawn, as it may fail to resolve paths if relevant mod settings have changed since then. + public Dictionary>?[] GetGameObjectResourcePaths(params ushort[] gameObjects); + + /// + /// Get the player and player-owned game objects' resources, as dictionaries of actual paths (that may be FS paths for redirected resources, or game paths for swapped or vanilla resources) to game paths. + /// + /// A dictionary of game object indices to resource path dictionaries. + /// This function is best called right after the game objects are redrawn, as it may fail to resolve paths if relevant mod settings have changed since then. + public Dictionary>> GetPlayerResourcePaths(); + + /// + /// Get the given game objects' resources of a given type, as dictionaries of resource handles to actual paths and, optionally, names and icons. + /// + /// Type of the resources to get, for example for materials. + /// Whether to get names and icons along with the paths. + /// The game object indices for which to get the resources. + /// An array of resource information dictionaries, of the same length and in the same order as the given game object index array. + /// + /// It is the caller's responsibility to make sure the returned resource handles are still in use on the game object's draw object before using them. + /// Also, callers should not use UI data for non-UI purposes. + /// + public GameResourceDict?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData, + params ushort[] gameObjects); + + /// + /// Get the player and player-owned game objects' resources of a given type, as dictionaries of resource handles to actual paths and, optionally, names and icons. + /// + /// Type of the resources to get, for example for materials. + /// Whether to get names and icons along with the paths. + /// A dictionary of game object indices to resource information dictionaries. + /// + /// It is the caller's responsibility to make sure the returned resource handles are still in use on the game object's draw object before using them. + /// Also, callers should not use UI data for non-UI purposes. + /// + public Dictionary GetPlayerResourcesOfType(ResourceType type, bool withUiData); + + /// + /// Get the given game objects' resource tree. + /// + /// Whether to get names and icons along with the paths. + /// The game object indices for which to get the resources. + /// An array of resource tree JObjects, of the same length and in the same order as the given game object index array. + /// + /// It is the caller's responsibility to make sure the returned resource handles are still in use on the game object's draw object before using them. + /// Also, callers should not use UI data for non-UI purposes. + /// + public JObject?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects); + + /// + /// Get the player and player-owned game objects' resource trees. + /// + /// Whether to get names and icons along with the paths. + /// A dictionary of game object indices to resource trees. + /// + /// It is the caller's responsibility to make sure the returned resource handles are still in use on the game object's draw object before using them. + /// Also, callers should not use UI data for non-UI purposes. + /// + public Dictionary GetPlayerResourceTrees(bool withUiData); +} diff --git a/Penumbra.Api/Api/Temporary.cs b/Penumbra.Api/Api/Temporary.cs new file mode 100644 index 0000000..6fcc02e --- /dev/null +++ b/Penumbra.Api/Api/Temporary.cs @@ -0,0 +1,147 @@ +using Penumbra.Api.Enums; + +namespace Penumbra.Api.Api; + +/// API methods pertaining to the management of temporary collections and mods. +public interface IPenumbraApiTemporary +{ + /// Temporarily set the settings of a mod in a collection to given values. + /// The collection to manipulate. + /// Specify the mod via its directory name. + /// Specify the mod via its (non-unique) display name. + /// Whether the mod should be forced to inherit from parent collections (if this is true, the other settings do not matter). + /// Whether the mod should be enabled or disabled. + /// The desired priority for the mod. + /// The new settings for the mod, as a map of Group Name -> All enabled Options (should be only one for single select groups). + /// A string to describe the source of those temporary settings. This is displayed to the user. + /// An optional lock to prevent other plugins and the user from changing these settings. Changes in mod structure will still remove the settings. Use 0 for no lock, or negative numbers for an identification lock that does not prevent the user from editing the temporary settings, but allows you to use with the same key to only remove your settings. + /// Success, CollectionMissing if the collection does not exist, TemporarySettingImpossible if the collection can not have settings, ModMissing if the mod can not be identified, TemporarySettingDisallowed if there is already a temporary setting with a different key, OptionGroupMissing if a group can not be found, OptionMissing if an option can not be found. + /// If not all groups are set in , they will be set to their default settings. + public PenumbraApiEc SetTemporaryModSettings(Guid collectionId, string modDirectory, string modName, bool inherit, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key); + + /// Temporarily set the settings of a mod in a collection to given values. + /// The game object index of the object whose collection you want to change. + /// Specify the mod via its directory name. + /// Specify the mod via its (non-unique) display name. + /// Whether the mod should be forced to inherit from parent collections (if this is true, the other settings do not matter). + /// Whether the mod should be enabled or disabled. + /// The desired priority for the mod. + /// The new settings for the mod, as a map of Group Name -> All enabled Options (should be only one for single select groups). + /// A string to describe the source of those temporary settings. This is displayed to the user. + /// An optional lock to prevent other plugins and the user from changing these settings. Changes in mod structure will still remove the settings. Use 0 for no lock. + /// Success, InvalidArgument if the game object does not exist, TemporarySettingImpossible if the collection can not have settings, ModMissing if the mod can not be identified, TemporarySettingDisallowed if there is already a temporary setting with a different key, OptionGroupMissing if a group can not be found, OptionMissing if an option can not be found. + /// If not all groups are set in , they will be set to their default settings. + public PenumbraApiEc SetTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool inherit, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key); + + /// Temporarily set the settings of a mod in a collection to given values. + /// The collection to manipulate. + /// Specify the mod via its directory name. + /// Specify the mod via its (non-unique) display name. + /// An optional key to a potential lock applied to those settings. + /// Success, NothingDone if no temporary settings could be removed with this key, CollectionMissing if the collection does not exist, TemporarySettingDisallowed if the key did not correspond to the lock. + public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key); + + /// Temporarily set the settings of a mod in a collection to given values. + /// The game object index of the object whose collection you want to change. + /// Specify the mod via its directory name. + /// Specify the mod via its (non-unique) display name. + /// An optional key to a potential lock applied to those settings. + /// Success, NothingDone if the mod did not have temporary settings in this collection, InvalidArgument if the game object does not exist, TemporarySettingDisallowed if the key did not correspond to the lock. + public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key); + + /// Temporarily set the settings of a mod in a collection to given values. + /// The collection to manipulate. + /// An optional key to a lock applied to those settings. All settings that use this key will be removed, all others ignored. + /// Success, NothingDone if no temporary settings could be removed with this key, CollectionMissing if the collection does not exist. + public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key); + + /// Temporarily set the settings of a mod in a collection to given values. + /// The game object index of the object whose collection you want to change. + /// An optional key to a lock applied to those settings. All settings that can be removed with this key will be removed, all others ignored. + /// Success, NothingDone if no temporary settings could be removed with this key, InvalidArgument if the game object does not exist. + public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key); + + /// Create a temporary collection. + /// The name for the collection. Arbitrary and only used internally for debugging. + /// The GUID of the created temporary collection. + public Guid CreateTemporaryCollection(string name); + + /// Remove the temporary collection of the given name. + /// The chosen temporary collection to remove. + /// NothingChanged or Success. + public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId); + + /// + /// Assign an existing temporary collection to an actor that currently occupies a specific slot. + /// + /// The chosen collection assigned to the actor. + /// The current object table index of the actor. + /// Whether to assign even if the actor is already assigned either a temporary or a permanent collection. + /// Success, InvalidArgument if the actor can not be identified, CollectionMissing if the collection does not exist, CharacterCollectionExists if is false and the actor is already assigned a collection, and AssignmentDeletionFailed if is true and the existing temporary assignment could not be deleted. + public PenumbraApiEc AssignTemporaryCollection(Guid collectionId, int actorIndex, bool forceAssignment); + + /// + /// Set a temporary mod with the given paths, manipulations and priority and the name tag to all regular and temporary collections. + /// + /// Custom name for the temporary mod. + /// List of redirections (can be swaps or redirections). + /// Zipped Base64 string of meta manipulations. + /// Desired priority. + /// InvalidGamePath, InvalidManipulation or Success. + public PenumbraApiEc AddTemporaryModAll(string tag, Dictionary paths, string manipString, int priority); + + /// Set a temporary mod with the given paths, manipulations and priority and the name tag to a specific collection. + /// + /// Custom name for the temporary mod. + /// GUID of the collection the mod should apply to. Can be a temporary collection. + /// List of redirections (can be swaps or redirections). + /// Zipped Base64 string of meta manipulations. + /// Desired priority. + /// CollectionMissing, InvalidGamePath, InvalidManipulation, InvalidArgument (GUID is nil) or Success. + public PenumbraApiEc AddTemporaryMod(string tag, Guid collectionId, Dictionary paths, string manipString, + int priority); + + /// + /// Remove the temporary mod with the given tag and priority from the temporary mods applying to all collections, if it exists. + /// + /// The tag to look for. + /// The initially provided priority. + /// NothingDone or Success. + public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority); + + /// + /// Remove the temporary mod with the given tag and priority from the temporary mods applying to a specific collection, if it exists. + /// + /// The tag to look for. + /// GUID of the collection the mod should apply to. Can be a temporary collection. + /// The initially provided priority. + /// CollectionMissing, NothingDone or Success. + public PenumbraApiEc RemoveTemporaryMod(string tag, Guid collectionId, int priority); + + /// Get the current temporary settings of a mod in the given collection. + /// The collection to query. + /// Specify the mod via its directory name. + /// Specify the mod via its (non-unique) display name. + /// The key for the settings lock. + /// + /// The settings as (ForceInherit, Enabled, Priority, Settings) or null if none are registered, + /// the registered source for the temporary settings or empty, + /// and Success, CollectionMissing, ModMissing or TemporarySettingDisallowed if the used key was > 0 and different from the provided key. + /// + public (PenumbraApiEc ErrorCode, (bool, bool, int, Dictionary>)?, string) QueryTemporaryModSettings(Guid collectionId, string modDirectory, + string modName, int key); + + /// Get the current temporary settings of a mod in the collection assigned to a given game object. + /// The game object index of the object whose collection you want to change. + /// Specify the mod via its directory name. + /// Specify the mod via its (non-unique) display name. + /// The key for the settings lock. + /// + /// The settings as (ForceInherit, Enabled, Priority, Settings) or null if none are registered, + /// the registered source for the temporary settings or empty, + /// and Success, InvalidArgument if the game object does not exist, ModMissing, or TemporarySettingDisallowed if the used key was > 0 and different from the provided key. + /// + public (PenumbraApiEc ErrorCode, (bool, bool, int, Dictionary>)? Settings, string Source) QueryTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key); +} diff --git a/Penumbra.Api/Api/Ui.cs b/Penumbra.Api/Api/Ui.cs new file mode 100644 index 0000000..1207623 --- /dev/null +++ b/Penumbra.Api/Api/Ui.cs @@ -0,0 +1,59 @@ +using Penumbra.Api.Enums; + +namespace Penumbra.Api.Api; + +/// API methods pertaining to Penumbras UI. +public interface IPenumbraApiUi +{ + /// + /// Triggered when the user hovers over a listed changed object in a mod tab. + /// Can be used to append tooltips. + /// + /// The type of the changed item and its ID if known. + public event Action? ChangedItemTooltip; + + /// + /// Triggered when the user clicks a listed changed object in a mod tab. + /// + /// The mouse button clicked, the type of the changed item and its ID if known. + public event Action? ChangedItemClicked; + + /// + /// Triggered before the settings tab bar for a mod is drawn, after the title group is drawn. + /// + /// The directory name of the currently selected mod, the total used width of the title bar and the width of the title box. + public event Action? PreSettingsTabBarDraw; + + /// + /// Triggered before the content of a mod settings panel is drawn. + /// + /// The directory name of the currently selected mod. + public event Action? PreSettingsPanelDraw; + + /// + /// Triggered after the Enabled Checkbox line in settings is drawn, but before options are drawn. + /// + /// The directory name of the currently selected mod. + public event Action? PostEnabledDraw; + + /// + /// Triggered after the content of a mod settings panel is drawn, but still in the child window. + /// + /// The directory name of the currently selected mod. + public event Action? PostSettingsPanelDraw; + + /// + /// Open the Penumbra main config window. + /// + /// Open the window at a specific tab. Use TabType.None to not change the tab. + /// Select a mod specified via its directory name in the mod tab, empty if none. + /// Select a mod specified via its mod name in the mod tab, empty if none. + /// InvalidArgument if is invalid, + /// ModMissing if or are set non-empty and the mod does not exist, + /// Success otherwise. + /// If is not TabType.Mods, the mod will not be selected regardless of other parameters and ModMissing will not be returned. + public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName); + + /// Close the Penumbra main config window. + public void CloseMainWindow(); +} diff --git a/Penumbra.Api/Delegates.cs b/Penumbra.Api/Delegates.cs new file mode 100644 index 0000000..b382cec --- /dev/null +++ b/Penumbra.Api/Delegates.cs @@ -0,0 +1,42 @@ +using Penumbra.Api.Enums; + +namespace Penumbra.Api; + +/// Used when a game object is redrawn by Penumbra. +/// The to the redrawn object and its . +public delegate void GameObjectRedrawnDelegate(nint objectPtr, int objectTableIndex); + +/// +/// Used when the setting of a mod is changed in any way. +/// +/// The of change, +/// the in which the setting is changed, +/// the name of the mod, +/// and whether the change was or not. +public delegate void ModSettingChangedDelegate(ModSettingChange type, Guid collectionId, string modDirectory, bool inherited); + +/// +/// Used before a new character base draw object is created from a . +/// +/// A pointer to the source , +/// the used for the object, +/// a pointer to the used (of type ushort*), +/// a pointer to the array, +/// and a pointer to the array. +public delegate void CreatingCharacterBaseDelegate(nint gameObject, Guid collectionId, nint modelId, nint customize, nint equipData); + +/// +/// Used after a character base has been created from a . +/// +/// A pointer to the source , +/// the used for the object, +/// a pointer to newly created . +public delegate void CreatedCharacterBaseDelegate(nint gameObject, Guid collectionId, nint drawObject); + +/// +/// Used when a specific game object has resolved a path to a non-default path. +/// +/// A pointer to the source , +/// the original that was resolved by Penumbra, +/// the resulting returned by Penumbra. +public delegate void GameObjectResourceResolvedDelegate(nint gameObject, string gamePath, string localPath); diff --git a/Penumbra.Api/Enums/ApiCollectionType.cs b/Penumbra.Api/Enums/ApiCollectionType.cs new file mode 100644 index 0000000..2a2198a --- /dev/null +++ b/Penumbra.Api/Enums/ApiCollectionType.cs @@ -0,0 +1,97 @@ +namespace Penumbra.Api.Enums; + +public enum ApiCollectionType : byte +{ + Yourself = 0, + + MalePlayerCharacter, + FemalePlayerCharacter, + MaleNonPlayerCharacter, + FemaleNonPlayerCharacter, + NonPlayerChild, + NonPlayerElderly, + + MaleMidlander, + FemaleMidlander, + MaleHighlander, + FemaleHighlander, + + MaleWildwood, + FemaleWildwood, + MaleDuskwight, + FemaleDuskwight, + + MalePlainsfolk, + FemalePlainsfolk, + MaleDunesfolk, + FemaleDunesfolk, + + MaleSeekerOfTheSun, + FemaleSeekerOfTheSun, + MaleKeeperOfTheMoon, + FemaleKeeperOfTheMoon, + + MaleSeawolf, + FemaleSeawolf, + MaleHellsguard, + FemaleHellsguard, + + MaleRaen, + FemaleRaen, + MaleXaela, + FemaleXaela, + + MaleHelion, + FemaleHelion, + MaleLost, + FemaleLost, + + MaleRava, + FemaleRava, + MaleVeena, + FemaleVeena, + + MaleMidlanderNpc, + FemaleMidlanderNpc, + MaleHighlanderNpc, + FemaleHighlanderNpc, + + MaleWildwoodNpc, + FemaleWildwoodNpc, + MaleDuskwightNpc, + FemaleDuskwightNpc, + + MalePlainsfolkNpc, + FemalePlainsfolkNpc, + MaleDunesfolkNpc, + FemaleDunesfolkNpc, + + MaleSeekerOfTheSunNpc, + FemaleSeekerOfTheSunNpc, + MaleKeeperOfTheMoonNpc, + FemaleKeeperOfTheMoonNpc, + + MaleSeawolfNpc, + FemaleSeawolfNpc, + MaleHellsguardNpc, + FemaleHellsguardNpc, + + MaleRaenNpc, + FemaleRaenNpc, + MaleXaelaNpc, + FemaleXaelaNpc, + + MaleHelionNpc, + FemaleHelionNpc, + MaleLostNpc, + FemaleLostNpc, + + MaleRavaNpc, + FemaleRavaNpc, + MaleVeenaNpc, + FemaleVeenaNpc, + + Default = 0xE0, + Interface = 0xE1, + Current = 0xE2, +} diff --git a/Penumbra.Api/Enums/ChangedItemIcon.cs b/Penumbra.Api/Enums/ChangedItemIcon.cs new file mode 100644 index 0000000..b47cd8d --- /dev/null +++ b/Penumbra.Api/Enums/ChangedItemIcon.cs @@ -0,0 +1,23 @@ +namespace Penumbra.Api.Enums; + +public enum ChangedItemIcon : uint +{ + None = 0, + Unknown = 1, + Head = 2, + Body = 3, + Hands = 4, + Legs = 5, + Feet = 6, + Ears = 7, + Neck = 8, + Wrists = 9, + Finger = 10, + Mainhand = 11, + Offhand = 12, + Customization = 13, + Monster = 14, + Demihuman = 15, + Action = 16, + Emote = 17, +} diff --git a/Penumbra.Api/Enums/ChangedItemType.cs b/Penumbra.Api/Enums/ChangedItemType.cs new file mode 100644 index 0000000..1e3bac5 --- /dev/null +++ b/Penumbra.Api/Enums/ChangedItemType.cs @@ -0,0 +1,17 @@ +namespace Penumbra.Api.Enums; + +/// +/// Describes known types of changed items that could provide special care. +/// +public enum ChangedItemType +{ + None = 0, + Item = 1, + Action = 2, + Customization = 3, + ItemOffhand = 4, + Unknown = 5, + Emote = 6, + Model = 7, + CustomArmor = 8, +} diff --git a/Penumbra.Api/Enums/GroupType.cs b/Penumbra.Api/Enums/GroupType.cs new file mode 100644 index 0000000..e67df34 --- /dev/null +++ b/Penumbra.Api/Enums/GroupType.cs @@ -0,0 +1,39 @@ +namespace Penumbra.Api.Enums; + +/// +/// The selection type for mod option groups. +/// +public enum GroupType +{ + /// + /// Exactly one option of this group has to be selected (if any exist). + /// + Single, + + /// + /// Any number of options in this group can be toggled on or off at the same time. + /// Limits the number of options in a single group to 32 at the most. + /// Each option is its own data container, which are independent of each other. + /// + Multi, + + /// + /// Any number of options in this group can be toggled on or off at the same time. + /// Affects a single IMC entry, to manipulate different parts of a model in a user-facing way. + /// + Imc, + + /// + /// Any number of options in this group can be toggled on or off at the same time. + /// Limits the number of options in a single group to 8 at the most. + /// Each combination of options is its own data container, resulting in 2^N separate data containers. + /// + Combining, + + /// + /// A group consisting of multiple separate subgroups where the options can depend on each other. + /// Each subgroup behaves the same way as its regular group type, just with optional dependencies on the other options. + /// The total number of options is still limited by the settings bit size. + /// + Complex, +} diff --git a/Penumbra.Api/Enums/ModSettingChange.cs b/Penumbra.Api/Enums/ModSettingChange.cs new file mode 100644 index 0000000..4edf8bd --- /dev/null +++ b/Penumbra.Api/Enums/ModSettingChange.cs @@ -0,0 +1,34 @@ +namespace Penumbra.Api.Enums; + +/// +/// Describes the way a mod can change its settings. +/// +public enum ModSettingChange +{ + /// It was set to inherit from other collections or not to inherit anymore. + Inheritance, + + /// It was enabled or disabled. + EnableState, + + /// Its priority was changed. + Priority, + + /// A specific setting for an option group was changed. + Setting, + + /// Multiple mods were set to inherit from other collections or not inherit anymore at once. + MultiInheritance, + + /// Multiple mods were enabled or disabled at once. + MultiEnableState, + + /// A temporary mod was enabled or disabled. + TemporaryMod, + + /// A mod was edited. Only invoked on edits affecting the current players collection and for that for now. + Edited, + + /// A temporary setting was added, removed or changed. + TemporarySetting, +} diff --git a/Penumbra.Api/Enums/MouseButton.cs b/Penumbra.Api/Enums/MouseButton.cs new file mode 100644 index 0000000..e5339e9 --- /dev/null +++ b/Penumbra.Api/Enums/MouseButton.cs @@ -0,0 +1,12 @@ +namespace Penumbra.Api.Enums; + +/// +/// Describes which mouse button was used to click an element. +/// +public enum MouseButton +{ + None, + Left, + Right, + Middle, +} diff --git a/Penumbra.Api/Enums/PenumbraApiEc.cs b/Penumbra.Api/Enums/PenumbraApiEc.cs new file mode 100644 index 0000000..27c00d6 --- /dev/null +++ b/Penumbra.Api/Enums/PenumbraApiEc.cs @@ -0,0 +1,33 @@ +namespace Penumbra.Api.Enums; + +/// +/// Error codes returned by some Penumbra.Api calls. +/// +public enum PenumbraApiEc +{ + Success = 0, + NothingChanged = 1, + CollectionMissing = 2, + ModMissing = 3, + OptionGroupMissing = 4, + OptionMissing = 5, + + CharacterCollectionExists = 6, + LowerPriority = 7, + InvalidGamePath = 8, + FileMissing = 9, + InvalidManipulation = 10, + InvalidArgument = 11, + PathRenameFailed = 12, + CollectionExists = 13, + AssignmentCreationDisallowed = 14, + AssignmentDeletionDisallowed = 15, + InvalidIdentifier = 16, + SystemDisposed = 17, + AssignmentDeletionFailed = 18, + + TemporarySettingDisallowed = 19, + TemporarySettingImpossible = 20, + + UnknownError = 255, +} diff --git a/Penumbra.Api/Enums/RedrawType.cs b/Penumbra.Api/Enums/RedrawType.cs new file mode 100644 index 0000000..07faacf --- /dev/null +++ b/Penumbra.Api/Enums/RedrawType.cs @@ -0,0 +1,11 @@ +namespace Penumbra.Api.Enums; + +/// +/// The way a specific game object shall be redrawn. +/// Actors can be redrawn immediately or after GPose. +/// +public enum RedrawType +{ + Redraw, + AfterGPose, +} diff --git a/Penumbra.Api/Enums/ResourceType.cs b/Penumbra.Api/Enums/ResourceType.cs new file mode 100644 index 0000000..c08c966 --- /dev/null +++ b/Penumbra.Api/Enums/ResourceType.cs @@ -0,0 +1,79 @@ +namespace Penumbra.Api.Enums; + +public enum ResourceType : uint +{ + Unknown = 0, + Aet = 0x00616574, + Amb = 0x00616D62, + Atch = 0x61746368, + Atex = 0x61746578, + Avfx = 0x61766678, + Awt = 0x00617774, + Bklb = 0x626B6C62, + Cmp = 0x00636D70, + Cutb = 0x63757462, + Dic = 0x00646963, + Eanb = 0x65616E62, + Eid = 0x00656964, + Envb = 0x656E7662, + Eqdp = 0x65716470, + Eqp = 0x00657170, + Eslb = 0x65736C63, + Essb = 0x65737362, + Est = 0x00657374, + Evp = 0x00657670, + Exd = 0x00657864, + Exh = 0x00657868, + Exl = 0x0065786C, + Fdt = 0x00666474, + Fpeb = 0x66706562, + Gfd = 0x00676664, + Ggd = 0x00676764, + Gmp = 0x00676D70, + Gzd = 0x00677A64, + Imc = 0x00696D63, + Kdb = 0x006B6462, + Kdlb = 0x6B646C62, + Lcb = 0x006C6362, + Lgb = 0x006C6762, + Luab = 0x6C756162, + Lvb = 0x006C7662, + Mdl = 0x006D646C, + Mlt = 0x006D6C74, + Mtrl = 0x6D74726C, + Obsb = 0x6F627362, + Pap = 0x00706170, + Pbd = 0x00706264, + Pcb = 0x00706362, + Phyb = 0x70687962, + Plt = 0x00706C74, + Scd = 0x00736364, + Sgb = 0x00736762, + Shcd = 0x73686364, + Shpk = 0x7368706B, + Sklb = 0x736B6C62, + Skp = 0x00736B70, + Stm = 0x0073746D, + Svb = 0x00737662, + Tera = 0x74657261, + Tex = 0x00746578, + Tmb = 0x00746D62, + Ugd = 0x00756764, + Uld = 0x00756C64, + Waoe = 0x77616F65, + Wtd = 0x00777464, +} + +public static class ResourceTypeExtensions +{ + public static ResourceType FromExtension(ReadOnlySpan ext) + => ext.Length switch + { + 0 => ResourceType.Unknown, + 1 => (ResourceType)(ext[0] | 32), + 2 => (ResourceType)(ext[0] | 32 | ((ext[1] | 32) << 8)), + 3 => (ResourceType)(ext[0] | 32 | ((ext[1] | 32) << 8) | ((ext[2] | 32) << 16)), + 4 => (ResourceType)(ext[0] | 32 | ((ext[1] | 32) << 8) | ((ext[2] | 32) << 16) | ((ext[2] | 32) << 24)), + _ => ResourceType.Unknown, + }; +} diff --git a/Penumbra.Api/Enums/TabType.cs b/Penumbra.Api/Enums/TabType.cs new file mode 100644 index 0000000..91c4789 --- /dev/null +++ b/Penumbra.Api/Enums/TabType.cs @@ -0,0 +1,19 @@ +namespace Penumbra.Api.Enums; + +/// +/// The different tabs of the main window that are available. +/// +public enum TabType +{ + None = -1, + Settings = 0, + Mods = 1, + Collections = 2, + ChangedItems = 3, + EffectiveChanges = 4, + ResourceWatcher = 5, + Debug = 6, + ResourceManager = 7, + OnScreen = 8, + Messages = 9, +} diff --git a/Penumbra.Api/Enums/TextureType.cs b/Penumbra.Api/Enums/TextureType.cs new file mode 100644 index 0000000..71382b6 --- /dev/null +++ b/Penumbra.Api/Enums/TextureType.cs @@ -0,0 +1,37 @@ +namespace Penumbra.Api.Enums; + +/// +/// The different types of textures a given texture can be converted to. +/// +public enum TextureType +{ + /// Convert the texture to .png. + Png = 0, + + /// Keep the texture format as it is but save as .tex. + AsIsTex = 1, + + /// Keep the texture format as it is but save as .dds. + AsIsDds = 2, + + /// Convert the texture to RGBA32 and save as .tex. + RgbaTex = 3, + + /// Convert the texture to RGBA32 and save as .dds. + RgbaDds = 4, + + /// Convert the texture to BC3 and save as .tex. + Bc3Tex = 5, + + /// Convert the texture to BC3 and save as .dds. + Bc3Dds = 6, + + /// Convert the texture to BC3 and save as .tex. + Bc7Tex = 7, + + /// Convert the texture to BC3 and save as .dds. + Bc7Dds = 8, + + /// Convert the texture to .tga. + Targa = 9, +} diff --git a/Penumbra.Api/GlobalUsings.cs b/Penumbra.Api/GlobalUsings.cs new file mode 100644 index 0000000..1472899 --- /dev/null +++ b/Penumbra.Api/GlobalUsings.cs @@ -0,0 +1,5 @@ +// Global using directives + +global using System; +global using System.Collections.Generic; +global using System.Threading.Tasks; diff --git a/Penumbra.Api/Helpers/ActionProvider.cs b/Penumbra.Api/Helpers/ActionProvider.cs new file mode 100644 index 0000000..f8343fa --- /dev/null +++ b/Penumbra.Api/Helpers/ActionProvider.cs @@ -0,0 +1,136 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; + +namespace Penumbra.Api.Helpers; + +/// +/// Specialized disposable Provider for Actions. +/// +public sealed class ActionProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public ActionProvider(IDalamudPluginInterface pi, string label, Action action) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterAction(action); + } + + public void Dispose() + { + _provider?.UnregisterAction(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~ActionProvider() + => Dispose(); +} + +/// +/// Specialized disposable Provider for Actions. +/// +public sealed class ActionProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public ActionProvider(IDalamudPluginInterface pi, string label, Action action) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterAction(action); + } + + public void Dispose() + { + _provider?.UnregisterAction(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~ActionProvider() + => Dispose(); +} + +/// +/// +/// +public sealed class ActionProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public ActionProvider(IDalamudPluginInterface pi, string label, Action action) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterAction(action); + } + + public void Dispose() + { + _provider?.UnregisterAction(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~ActionProvider() + => Dispose(); +} + +/// +/// +/// +public sealed class ActionProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public ActionProvider(IDalamudPluginInterface pi, string label, Action action) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterAction(action); + } + + public void Dispose() + { + _provider?.UnregisterAction(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~ActionProvider() + => Dispose(); +} diff --git a/Penumbra.Api/Helpers/ActionSubscriber.cs b/Penumbra.Api/Helpers/ActionSubscriber.cs new file mode 100644 index 0000000..604b765 --- /dev/null +++ b/Penumbra.Api/Helpers/ActionSubscriber.cs @@ -0,0 +1,114 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; + +namespace Penumbra.Api.Helpers; + +/// +/// Specialized subscriber only allowing to invoke actions. +/// +public class ActionSubscriber +{ + private readonly ICallGateSubscriber? _subscriber; + + /// Whether the subscriber could successfully be created. + public bool Valid + => _subscriber != null; + + protected ActionSubscriber(IDalamudPluginInterface pi, string label) + { + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// Invoke the action. See the source of the subscriber for details. + protected void Invoke() + => _subscriber?.InvokeAction(); +} + +/// +public class ActionSubscriber +{ + private readonly ICallGateSubscriber? _subscriber; + + /// Whether the subscriber could successfully be created. + public bool Valid + => _subscriber != null; + + protected ActionSubscriber(IDalamudPluginInterface pi, string label) + { + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// Invoke the action. See the source of the subscriber for details. + protected void Invoke(T1 a) + => _subscriber?.InvokeAction(a); +} + +/// +public class ActionSubscriber +{ + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + protected ActionSubscriber(IDalamudPluginInterface pi, string label) + { + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected void Invoke(T1 a, T2 b) + => _subscriber?.InvokeAction(a, b); +} + +/// +public class ActionSubscriber +{ + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + protected ActionSubscriber(IDalamudPluginInterface pi, string label) + { + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected void Invoke(T1 a, T2 b, T3 c) + => _subscriber?.InvokeAction(a, b, c); +} diff --git a/Penumbra.Api/Helpers/ConvertingDict.cs b/Penumbra.Api/Helpers/ConvertingDict.cs new file mode 100644 index 0000000..9c1fef5 --- /dev/null +++ b/Penumbra.Api/Helpers/ConvertingDict.cs @@ -0,0 +1,152 @@ +using System.Collections; +using System.Runtime.CompilerServices; + +namespace Penumbra.Api.Helpers; + +/// A dictionary that implicitly can be converted to a read-only dictionary with different value type. +/// The type of the keys. +/// The actual type of the values. +/// The read-only type of the values. +public abstract class ConvertingDict(IReadOnlyDictionary dict) + : IReadOnlyDictionary + where TKey : notnull +{ + /// Obtain the original dictionary. + public IReadOnlyDictionary Original + => dict; + + /// Conversion between values. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected abstract TValueTo ConvertValue(in TValueFrom from); + + /// + public bool ContainsKey(TKey key) + => dict.ContainsKey(key); + + /// + public bool TryGetValue(TKey key, out TValueTo value) + { + if (dict.TryGetValue(key, out var v)) + { + value = ConvertValue(v); + return true; + } + + value = default!; + return false; + } + + /// + public TValueTo this[TKey key] + => ConvertValue(dict[key]); + + public IEnumerable Keys + => dict.Keys; + + /// + public IEnumerable Values + { + get + { + foreach (var value in dict.Values) + yield return ConvertValue(value); + } + } + + /// + public int Count + => dict.Count; + + /// + public IEnumerator> GetEnumerator() + { + foreach (var kvp in dict) + yield return new KeyValuePair(kvp.Key, ConvertValue(kvp.Value)); + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); +} + +/// A dictionary that implicitly can be converted to a read-only dictionary with different value type. +/// The actual type of the keys. +/// The read-only type of the keys. +/// The actual type of the values. +/// The read-only type of the values. +public abstract class ConvertingDict(IReadOnlyDictionary dict) + : IReadOnlyDictionary + where TKeyFrom : notnull + where TKeyTo : notnull +{ + /// Obtain the original dictionary. + public IReadOnlyDictionary Original + => dict; + + /// Conversion between keys. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected abstract TKeyTo ConvertKey(in TKeyFrom from); + + /// Conversion between keys. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected abstract TKeyFrom ConvertKeyBack(in TKeyTo from); + + /// Conversion between values. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected abstract TValueTo ConvertValue(in TValueFrom from); + + /// + public bool ContainsKey(TKeyTo key) + => dict.ContainsKey(ConvertKeyBack(key)); + + /// + public bool TryGetValue(TKeyTo key, out TValueTo value) + { + if (dict.TryGetValue(ConvertKeyBack(key), out var v)) + { + value = ConvertValue(v); + return true; + } + + value = default!; + return false; + } + + /// + public TValueTo this[TKeyTo key] + => ConvertValue(dict[ConvertKeyBack(key)]); + + /// + public IEnumerable Keys + { + get + { + foreach (var key in dict.Keys) + yield return ConvertKey(key); + } + } + + /// + public IEnumerable Values + { + get + { + foreach (var value in dict.Values) + yield return ConvertValue(value); + } + } + + /// + public IEnumerator> GetEnumerator() + { + foreach (var kvp in dict) + yield return new KeyValuePair(ConvertKey(kvp.Key), ConvertValue(kvp.Value)); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + /// + public int Count + => dict.Count; +} diff --git a/Penumbra.Api/Helpers/DtoClasses.cs b/Penumbra.Api/Helpers/DtoClasses.cs new file mode 100644 index 0000000..ac72e1b --- /dev/null +++ b/Penumbra.Api/Helpers/DtoClasses.cs @@ -0,0 +1,53 @@ +using System.Runtime.CompilerServices; +using Penumbra.Api.Enums; + +namespace Penumbra.Api.Helpers; + +/// Wrapper dictionary. +public sealed class GameResourceDict(IReadOnlyDictionary dict) + : ConvertingDict(dict) +{ + /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override (string, string, ChangedItemIcon) ConvertValue(in (string, string, uint) from) + => (from.Item1, from.Item2, (ChangedItemIcon)from.Item3); + + /// Create dictionary or null. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static GameResourceDict? Create(IReadOnlyDictionary? dict) + => dict == null ? null : new GameResourceDict(dict); +} + +/// Wrapper dictionary. +public sealed class AvailableModSettings(IReadOnlyDictionary dict) + : ConvertingDict(dict) +{ + /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override (string[], GroupType) ConvertValue(in (string[], int) from) + => (from.Item1, (GroupType)from.Item2); + + /// Create dictionary or null. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static AvailableModSettings? Create(IReadOnlyDictionary? dict) + => dict == null ? null : new AvailableModSettings(dict); +} + +public record ResourceNodeDto +{ + public required ResourceType Type { get; init; } + public required ChangedItemIcon Icon { get; init; } + public required string? Name { get; init; } + public required string? GamePath { get; init; } + public required string ActualPath { get; init; } + public required nint ObjectAddress { get; init; } + public required nint ResourceHandle { get; init; } + public required List Children { get; init; } +} + +public record ResourceTreeDto +{ + public required string Name { get; init; } + public required ushort RaceCode { get; init; } + public required List Nodes { get; init; } +} diff --git a/Penumbra.Api/Helpers/EventProvider.cs b/Penumbra.Api/Helpers/EventProvider.cs new file mode 100644 index 0000000..d5e0351 --- /dev/null +++ b/Penumbra.Api/Helpers/EventProvider.cs @@ -0,0 +1,465 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Services; + +namespace Penumbra.Api.Helpers; + +/// +/// Specialized disposable Provider for Events. +/// Will execute the unsubscriber action on dispose if any is provided. +/// Can only be invoked and disposed. +/// +public sealed class EventProvider : IDisposable +{ + private readonly IPluginLog _log; + private ICallGateProvider? _provider; + private Delegate? _unsubscriber; + + public EventProvider(IDalamudPluginInterface pi, string label, (Action Add, Action Del)? subscribe = null) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + subscribe?.Add(Invoke); + _unsubscriber = subscribe?.Del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + public EventProvider(IDalamudPluginInterface pi, string label, Action add, Action del) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + add(this); + _unsubscriber = del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + /// Invoke the event. + public void Invoke() + { + try + { + _provider?.SendMessage(); + } + catch (Exception e) + { + _log.Error($"Exception thrown on IPC event:\n{e}"); + } + } + + public void Dispose() + { + switch (_unsubscriber) + { + case Action a: + a(Invoke); + break; + case Action b: + b(this); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize(this); + } + + ~EventProvider() + => Dispose(); +} + +/// +public sealed class EventProvider : IDisposable +{ + private readonly IPluginLog _log; + private ICallGateProvider? _provider; + private Delegate? _unsubscriber; + + public EventProvider(IDalamudPluginInterface pi, string label, (Action> Add, Action> Del)? subscribe = null) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + subscribe?.Add(Invoke); + _unsubscriber = subscribe?.Del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + public EventProvider(IDalamudPluginInterface pi, string label, Action> add, Action> del) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + add(this); + _unsubscriber = del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + /// + public void Invoke(T1 a) + { + try + { + _provider?.SendMessage(a); + } + catch (Exception e) + { + _log.Error($"Exception thrown on IPC event:\n{e}"); + } + } + + public void Dispose() + { + switch (_unsubscriber) + { + case Action> a: + a(Invoke); + break; + case Action> b: + b(this); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize(this); + } + + ~EventProvider() + => Dispose(); +} + +/// +public sealed class EventProvider : IDisposable +{ + private readonly IPluginLog _log; + private ICallGateProvider? _provider; + private Delegate? _unsubscriber; + + public EventProvider(IDalamudPluginInterface pi, string label, + (Action> Add, Action> Del)? subscribe = null) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + subscribe?.Add(Invoke); + _unsubscriber = subscribe?.Del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + public EventProvider(IDalamudPluginInterface pi, string label, Action> add, Action> del) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + add(this); + _unsubscriber = del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + /// + public void Invoke(T1 a, T2 b) + { + try + { + _provider?.SendMessage(a, b); + } + catch (Exception e) + { + _log.Error($"Exception thrown on IPC event:\n{e}"); + } + } + + public void Dispose() + { + switch (_unsubscriber) + { + case Action> a: + a(Invoke); + break; + case Action> b: + b(this); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize(this); + } + + ~EventProvider() + => Dispose(); +} + +/// +public sealed class EventProvider : IDisposable +{ + private readonly IPluginLog _log; + private ICallGateProvider? _provider; + private Delegate? _unsubscriber; + + public EventProvider(IDalamudPluginInterface pi, string label, + (Action> Add, Action> Del)? subscribe = null) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + subscribe?.Add(Invoke); + _unsubscriber = subscribe?.Del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + public EventProvider(IDalamudPluginInterface pi, string label, Action> add, Action> del) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + add(this); + _unsubscriber = del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + /// + public void Invoke(T1 a, T2 b, T3 c) + { + try + { + _provider?.SendMessage(a, b, c); + } + catch (Exception e) + { + _log.Error($"Exception thrown on IPC event:\n{e}"); + } + } + + public void Dispose() + { + switch (_unsubscriber) + { + case Action> a: + a(Invoke); + break; + case Action> b: + b(this); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize(this); + } + + ~EventProvider() + => Dispose(); +} + +/// +public sealed class EventProvider : IDisposable +{ + private readonly IPluginLog _log; + private ICallGateProvider? _provider; + private Delegate? _unsubscriber; + + public EventProvider(IDalamudPluginInterface pi, string label, + (Action> Add, Action> Del)? subscribe = null) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + subscribe?.Add(Invoke); + _unsubscriber = subscribe?.Del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + public EventProvider(IDalamudPluginInterface pi, string label, Action> add, + Action> del) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + add(this); + _unsubscriber = del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + /// + public void Invoke(T1 a, T2 b, T3 c, T4 d) + { + try + { + _provider?.SendMessage(a, b, c, d); + } + catch (Exception e) + { + _log.Error($"Exception thrown on IPC event:\n{e}"); + } + } + + public void Dispose() + { + switch (_unsubscriber) + { + case Action> a: + a(Invoke); + break; + case Action> b: + b(this); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize(this); + } + + ~EventProvider() + => Dispose(); +} + +/// +public sealed class EventProvider : IDisposable +{ + private readonly IPluginLog _log; + private ICallGateProvider? _provider; + private Delegate? _unsubscriber; + + public EventProvider(IDalamudPluginInterface pi, string label, + (Action> Add, Action> Del)? subscribe = null) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + subscribe?.Add(Invoke); + _unsubscriber = subscribe?.Del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + public EventProvider(IDalamudPluginInterface pi, string label, Action> add, + Action> del) + { + _unsubscriber = null; + _log = PluginLogHelper.GetLog(pi); + try + { + _provider = pi.GetIpcProvider(label); + add(this); + _unsubscriber = del; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + } + + /// + public void Invoke(T1 a, T2 b, T3 c, T4 d, T5 e) + { + try + { + _provider?.SendMessage(a, b, c, d, e); + } + catch (Exception ex) + { + _log.Error($"Exception thrown on IPC event:\n{ex}"); + } + } + + public void Dispose() + { + switch (_unsubscriber) + { + case Action> a: + a(Invoke); + break; + case Action> b: + b(this); + break; + } + + _unsubscriber = null; + _provider = null; + GC.SuppressFinalize(this); + } + + ~EventProvider() + => Dispose(); +} diff --git a/Penumbra.Api/Helpers/EventSubscriber.cs b/Penumbra.Api/Helpers/EventSubscriber.cs new file mode 100644 index 0000000..c8da3ca --- /dev/null +++ b/Penumbra.Api/Helpers/EventSubscriber.cs @@ -0,0 +1,582 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Services; + +namespace Penumbra.Api.Helpers; + +/// +/// Specialized disposable Subscriber for Events. +/// Subscriptions are wrapped to be individually exception-safe. +/// Can be enabled and disabled. +/// +public sealed class EventSubscriber : IDisposable +{ + private readonly string _label; + private readonly IPluginLog _log; + private readonly Dictionary _delegates = new(); + private ICallGateSubscriber? _subscriber; + private bool _disabled; + + public EventSubscriber(IDalamudPluginInterface pi, string label, params Action[] actions) + { + _label = label; + _log = PluginLogHelper.GetLog(pi); + try + { + _subscriber = pi.GetIpcSubscriber(label); + foreach (var action in actions) + Event += action; + + _disabled = false; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + /// Enable all currently subscribed actions registered with this EventSubscriber. + /// Does nothing if it is already enabled. + /// + public void Enable() + { + if (_disabled && _subscriber != null) + { + foreach (var action in _delegates.Values) + _subscriber.Subscribe(action); + + _disabled = false; + } + } + + /// + /// Disable all subscribed actions registered with this EventSubscriber. + /// Does nothing if it is already disabled. + /// Does not forget the actions, only disables them. + /// + public void Disable() + { + if (!_disabled) + { + if (_subscriber != null) + foreach (var action in _delegates.Values) + _subscriber.Unsubscribe(action); + + _disabled = true; + } + } + + /// + /// Add or remove an action to the IPC event, if it is valid. + /// + public event Action Event + { + add + { + if (_subscriber != null && !_delegates.ContainsKey(value)) + { + void Action() + { + try + { + value(); + } + catch (Exception e) + { + _log.Error($"Exception invoking IPC event {_label}:\n{e}"); + } + } + + if (_delegates.TryAdd(value, Action) && !_disabled) + _subscriber.Subscribe(Action); + } + } + remove + { + if (_subscriber != null && _delegates.Remove(value, out var action)) + _subscriber.Unsubscribe(action); + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +/// +public sealed class EventSubscriber : IDisposable +{ + private readonly string _label; + private readonly IPluginLog _log; + private readonly Dictionary, Action> _delegates = new(); + private ICallGateSubscriber? _subscriber; + private bool _disabled; + + public EventSubscriber(IDalamudPluginInterface pi, string label, params Action[] actions) + { + _label = label; + _log = PluginLogHelper.GetLog(pi); + try + { + _subscriber = pi.GetIpcSubscriber(label); + foreach (var action in actions) + Event += action; + + _disabled = false; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + public void Enable() + { + if (_disabled && _subscriber != null) + { + foreach (var action in _delegates.Values) + _subscriber.Subscribe(action); + + _disabled = false; + } + } + + /// + public void Disable() + { + if (!_disabled) + { + if (_subscriber != null) + foreach (var action in _delegates.Values) + _subscriber.Unsubscribe(action); + + _disabled = true; + } + } + + /// + public event Action Event + { + add + { + if (_subscriber != null && !_delegates.ContainsKey(value)) + { + void Action(T1 a) + { + try + { + value(a); + } + catch (Exception e) + { + _log.Error($"Exception invoking IPC event {_label}:\n{e}"); + } + } + + if (_delegates.TryAdd(value, Action) && !_disabled) + _subscriber.Subscribe(Action); + } + } + remove + { + if (_subscriber != null && _delegates.Remove(value, out var action)) + _subscriber.Unsubscribe(action); + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +/// +public sealed class EventSubscriber : IDisposable +{ + private readonly string _label; + private readonly IPluginLog _log; + private readonly Dictionary, Action> _delegates = new(); + private ICallGateSubscriber? _subscriber; + private bool _disabled; + + public EventSubscriber(IDalamudPluginInterface pi, string label, params Action[] actions) + { + _label = label; + _log = PluginLogHelper.GetLog(pi); + try + { + _subscriber = pi.GetIpcSubscriber(label); + foreach (var action in actions) + Event += action; + + _disabled = false; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + public void Enable() + { + if (_disabled && _subscriber != null) + { + foreach (var action in _delegates.Values) + _subscriber.Subscribe(action); + + _disabled = false; + } + } + + /// + public void Disable() + { + if (!_disabled) + { + if (_subscriber != null) + foreach (var action in _delegates.Values) + _subscriber.Unsubscribe(action); + + _disabled = true; + } + } + + /// + public event Action Event + { + add + { + if (_subscriber != null && !_delegates.ContainsKey(value)) + { + void Action(T1 a, T2 b) + { + try + { + value(a, b); + } + catch (Exception e) + { + _log.Error($"Exception invoking IPC event {_label}:\n{e}"); + } + } + + if (_delegates.TryAdd(value, Action) && !_disabled) + _subscriber.Subscribe(Action); + } + } + remove + { + if (_subscriber != null && _delegates.Remove(value, out var action)) + _subscriber.Unsubscribe(action); + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +/// +public sealed class EventSubscriber : IDisposable +{ + private readonly string _label; + private readonly IPluginLog _log; + private readonly Dictionary, Action> _delegates = []; + private ICallGateSubscriber? _subscriber; + private bool _disabled; + + public EventSubscriber(IDalamudPluginInterface pi, string label, params Action[] actions) + { + _label = label; + _log = PluginLogHelper.GetLog(pi); + try + { + _subscriber = pi.GetIpcSubscriber(label); + foreach (var action in actions) + Event += action; + + _disabled = false; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + public void Enable() + { + if (_disabled && _subscriber != null) + { + foreach (var action in _delegates.Values) + _subscriber.Subscribe(action); + + _disabled = false; + } + } + + /// + public void Disable() + { + if (!_disabled) + { + if (_subscriber != null) + foreach (var action in _delegates.Values) + _subscriber.Unsubscribe(action); + + _disabled = true; + } + } + + /// + public event Action Event + { + add + { + if (_subscriber != null && !_delegates.ContainsKey(value)) + { + void Action(T1 a, T2 b, T3 c) + { + try + { + value(a, b, c); + } + catch (Exception e) + { + _log.Error($"Exception invoking IPC event {_label}:\n{e}"); + } + } + + if (_delegates.TryAdd(value, Action) && !_disabled) + _subscriber.Subscribe(Action); + } + } + remove + { + if (_subscriber != null && _delegates.Remove(value, out var action)) + _subscriber.Unsubscribe(action); + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +/// +public sealed class EventSubscriber : IDisposable +{ + private readonly string _label; + private readonly IPluginLog _log; + private readonly Dictionary, Action> _delegates = new(); + private ICallGateSubscriber? _subscriber; + private bool _disabled; + + public EventSubscriber(IDalamudPluginInterface pi, string label, params Action[] actions) + { + _label = label; + _log = PluginLogHelper.GetLog(pi); + try + { + _subscriber = pi.GetIpcSubscriber(label); + foreach (var action in actions) + Event += action; + + _disabled = false; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + public void Enable() + { + if (_disabled && _subscriber != null) + { + foreach (var action in _delegates.Values) + _subscriber.Subscribe(action); + + _disabled = false; + } + } + + /// + public void Disable() + { + if (!_disabled) + { + if (_subscriber != null) + foreach (var action in _delegates.Values) + _subscriber.Unsubscribe(action); + + _disabled = true; + } + } + + /// + public event Action Event + { + add + { + if (_subscriber != null && !_delegates.ContainsKey(value)) + { + void Action(T1 a, T2 b, T3 c, T4 d) + { + try + { + value(a, b, c, d); + } + catch (Exception e) + { + _log.Error($"Exception invoking IPC event {_label}:\n{e}"); + } + } + + if (_delegates.TryAdd(value, Action) && !_disabled) + _subscriber.Subscribe(Action); + } + } + remove + { + if (_subscriber != null && _delegates.Remove(value, out var action)) + _subscriber.Unsubscribe(action); + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} + +/// +public sealed class EventSubscriber : IDisposable +{ + private readonly string _label; + private readonly IPluginLog _log; + private readonly Dictionary, Action> _delegates = new(); + private ICallGateSubscriber? _subscriber; + private bool _disabled; + + public EventSubscriber(IDalamudPluginInterface pi, string label, params Action[] actions) + { + _label = label; + _log = PluginLogHelper.GetLog(pi); + try + { + _subscriber = pi.GetIpcSubscriber(label); + foreach (var action in actions) + Event += action; + + _disabled = false; + } + catch (Exception e) + { + _log.Error($"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + public void Enable() + { + if (_disabled && _subscriber != null) + { + foreach (var action in _delegates.Values) + _subscriber.Subscribe(action); + + _disabled = false; + } + } + + /// + public void Disable() + { + if (!_disabled) + { + if (_subscriber != null) + foreach (var action in _delegates.Values) + _subscriber.Unsubscribe(action); + + _disabled = true; + } + } + + /// + public event Action Event + { + add + { + if (_subscriber != null && !_delegates.ContainsKey(value)) + { + void Action(T1 a, T2 b, T3 c, T4 d, T5 e) + { + try + { + value(a, b, c, d, e); + } + catch (Exception ex) + { + _log.Error($"Exception invoking IPC event {_label}:\n{ex}"); + } + } + + if (_delegates.TryAdd(value, Action) && !_disabled) + _subscriber.Subscribe(Action); + } + } + remove + { + if (_subscriber != null && _delegates.Remove(value, out var action)) + _subscriber.Unsubscribe(action); + } + } + + public void Dispose() + { + Disable(); + _subscriber = null; + _delegates.Clear(); + } + + ~EventSubscriber() + => Dispose(); +} diff --git a/Penumbra.Api/Helpers/FuncProvider.cs b/Penumbra.Api/Helpers/FuncProvider.cs new file mode 100644 index 0000000..b70bd15 --- /dev/null +++ b/Penumbra.Api/Helpers/FuncProvider.cs @@ -0,0 +1,223 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; + +namespace Penumbra.Api.Helpers; + +/// +/// Specialized disposable Provider for Funcs. +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} + +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} + +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} + +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} + +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} + +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} + +/// +public sealed class FuncProvider : IDisposable +{ + private ICallGateProvider? _provider; + + public FuncProvider(IDalamudPluginInterface pi, string label, Func func) + { + try + { + _provider = pi.GetIpcProvider(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Provider for {label}\n{e}"); + _provider = null; + } + + _provider?.RegisterFunc(func); + } + + public void Dispose() + { + _provider?.UnregisterFunc(); + _provider = null; + GC.SuppressFinalize(this); + } + + ~FuncProvider() + => Dispose(); +} diff --git a/Penumbra.Api/Helpers/FuncSubscriber.cs b/Penumbra.Api/Helpers/FuncSubscriber.cs new file mode 100644 index 0000000..6bddf3e --- /dev/null +++ b/Penumbra.Api/Helpers/FuncSubscriber.cs @@ -0,0 +1,217 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Ipc.Exceptions; + +namespace Penumbra.Api.Helpers; + +/// +/// Specialized subscriber only allowing to invoke functions with a return. +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// Whether the subscriber could successfully be created. + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// Invoke the function. See the source of the subscriber for details. + protected TRet Invoke() + => _subscriber != null ? _subscriber.InvokeFunc() : throw new IpcNotReadyError(_label); +} + +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected TRet Invoke(T1 a) + => _subscriber != null ? _subscriber.InvokeFunc(a) : throw new IpcNotReadyError(_label); +} + +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected TRet Invoke(T1 a, T2 b) + => _subscriber != null ? _subscriber.InvokeFunc(a, b) : throw new IpcNotReadyError(_label); +} + +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected TRet Invoke(T1 a, T2 b, T3 c) + => _subscriber != null ? _subscriber.InvokeFunc(a, b, c) : throw new IpcNotReadyError(_label); +} + +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected TRet Invoke(T1 a, T2 b, T3 c, T4 d) + => _subscriber != null ? _subscriber.InvokeFunc(a, b, c, d) : throw new IpcNotReadyError(_label); +} + +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected TRet Invoke(T1 a, T2 b, T3 c, T4 d, T5 e) + => _subscriber != null ? _subscriber.InvokeFunc(a, b, c, d, e) : throw new IpcNotReadyError(_label); +} + +/// +public class FuncSubscriber +{ + private readonly string _label; + private readonly ICallGateSubscriber? _subscriber; + + /// + public bool Valid + => _subscriber != null; + + /// + protected FuncSubscriber(IDalamudPluginInterface pi, string label) + { + _label = label; + try + { + _subscriber = pi.GetIpcSubscriber(label); + } + catch (Exception e) + { + PluginLogHelper.WriteError(pi, $"Error registering IPC Subscriber for {label}\n{e}"); + _subscriber = null; + } + } + + /// + protected TRet Invoke(T1 a, T2 b, T3 c, T4 d, T5 e, T6 f) + => _subscriber != null ? _subscriber.InvokeFunc(a, b, c, d, e, f) : throw new IpcNotReadyError(_label); +} diff --git a/Penumbra.Api/Helpers/PluginLogHelper.cs b/Penumbra.Api/Helpers/PluginLogHelper.cs new file mode 100644 index 0000000..ecf4ce7 --- /dev/null +++ b/Penumbra.Api/Helpers/PluginLogHelper.cs @@ -0,0 +1,26 @@ +using Dalamud.IoC; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; + +namespace Penumbra.Api.Helpers; + +internal class PluginLogHelper +{ + [PluginService] + private static IPluginLog? _log { get; set; } + + private PluginLogHelper(IDalamudPluginInterface pi) + => pi.Inject(this); + + public static void WriteError(IDalamudPluginInterface pi, string errorMessage) + => GetLog(pi).Error(errorMessage); + + public static IPluginLog GetLog(IDalamudPluginInterface pi) + { + if (_log != null) + return _log; + + _ = new PluginLogHelper(pi); + return _log!; + } +} diff --git a/Penumbra.Api/IpcSubscribers/Collection.cs b/Penumbra.Api/IpcSubscribers/Collection.cs new file mode 100644 index 0000000..12358af --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Collection.cs @@ -0,0 +1,154 @@ +using Dalamud.Plugin; +using Penumbra.Api.Api; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.IpcSubscribers; + +/// +public sealed class GetCollections(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetCollections)}.V5"; + + /// + public new Dictionary Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider> Provider(IDalamudPluginInterface pi, IPenumbraApiCollection api) + => new(pi, Label, api.GetCollections); +} + +/// +public sealed class GetCollectionsByIdentifier(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetCollectionsByIdentifier)}"; + + /// + public new List<(Guid Id, string Name)> Invoke(string name) + => base.Invoke(name); + + /// Create a provider. + public static FuncProvider> Provider(IDalamudPluginInterface pi, IPenumbraApiCollection api) + => new(pi, Label, api.GetCollectionsByIdentifier); +} + +/// +public sealed class GetChangedItemsForCollection(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetChangedItemsForCollection)}"; + + /// + public new Dictionary Invoke(Guid collectionId) + => base.Invoke(collectionId); + + /// Create a provider. + public static FuncProvider> Provider(IDalamudPluginInterface pi, IPenumbraApiCollection api) + => new(pi, Label, api.GetChangedItemsForCollection); +} + +/// +public sealed class GetCollection(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetCollection)}"; + + /// + public (Guid Id, string Name)? Invoke(ApiCollectionType type) + => Invoke((byte)type); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiCollection api) + => new(pi, Label, b => api.GetCollection((ApiCollectionType)b)); +} + +/// +public sealed class GetCollectionForObject(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetCollectionForObject)}.V5"; + + /// + public new (bool ObjectValid, bool IndividualSet, (Guid Id, string Name) EffectiveCollection) Invoke(int gameObjectIdx) + => base.Invoke(gameObjectIdx); + + /// Create a provider. + public static FuncProvider + Provider(IDalamudPluginInterface pi, IPenumbraApiCollection api) + => new(pi, Label, api.GetCollectionForObject); +} + +/// +public sealed class SetCollection(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(SetCollection)}"; + + /// + public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) Invoke(ApiCollectionType type, Guid? collectionId, + bool allowCreateNew = true, bool allowDelete = true) + { + var (ec, pair) = Invoke((byte)type, collectionId, allowCreateNew, allowDelete); + return ((PenumbraApiEc)ec, pair); + } + + /// Create a provider. + public static FuncProvider + Provider(IDalamudPluginInterface pi, IPenumbraApiCollection api) + => new(pi, Label, (t, g, a, b) => + { + var (ret, collection) = api.SetCollection((ApiCollectionType)t, g, a, b); + return ((int)ret, collection); + }); +} + +/// +public sealed class SetCollectionForObject(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(SetCollectionForObject)}.V5"; + + /// + public new (PenumbraApiEc, (Guid Id, string Name)? OldCollection) Invoke(int gameObjectIdx, Guid? collectionId, bool allowCreateNew = true, + bool allowDelete = true) + { + var (ec, pair) = base.Invoke(gameObjectIdx, collectionId, allowCreateNew, allowDelete); + return ((PenumbraApiEc)ec, pair); + } + + /// Create a provider. + public static FuncProvider + Provider(IDalamudPluginInterface pi, IPenumbraApiCollection api) + => new(pi, Label, (i, g, a, b) => + { + var (ret, collection) = api.SetCollectionForObject(i, g, a, b); + return ((int)ret, collection); + }); +} + +/// +public sealed class CheckCurrentChangedItemFunc(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(CheckCurrentChangedItemFunc)}"; + + /// + public new Func Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider> + Provider(IDalamudPluginInterface pi, IPenumbraApiCollection api) + => new(pi, Label, api.CheckCurrentChangedItemFunc); +} diff --git a/Penumbra.Api/IpcSubscribers/Editing.cs b/Penumbra.Api/IpcSubscribers/Editing.cs new file mode 100644 index 0000000..753f342 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Editing.cs @@ -0,0 +1,38 @@ +using Dalamud.Plugin; +using Penumbra.Api.Api; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.IpcSubscribers; + +/// +public sealed class ConvertTextureFile(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ConvertTextureFile)}"; + + /// + public Task Invoke(string inputFile, string outputFile, TextureType textureType, bool mipMaps = true) + => Invoke(inputFile, outputFile, (int)textureType, mipMaps); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiEditing api) + => new(pi, Label, (a, b, c, d) => api.ConvertTextureFile(a, b, (TextureType)c, d)); +} + +/// +public sealed class ConvertTextureData(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ConvertTextureData)}"; + + /// + public Task Invoke(byte[] rgbaData, int width, string outputFile, TextureType textureType, bool mipMaps = true) + => Invoke(rgbaData, width, outputFile, (int)textureType, mipMaps); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiEditing api) + => new(pi, Label, (a, b, c, d, e) => api.ConvertTextureData(a, b, c, (TextureType)d, e)); +} diff --git a/Penumbra.Api/IpcSubscribers/GameState.cs b/Penumbra.Api/IpcSubscribers/GameState.cs new file mode 100644 index 0000000..7f14489 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/GameState.cs @@ -0,0 +1,100 @@ +using Dalamud.Plugin; +using Penumbra.Api.Api; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.IpcSubscribers; + +/// +public sealed class GetDrawObjectInfo(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetDrawObjectInfo)}.V5"; + + /// + public new (nint GameObject, (Guid Id, string Name) AssociatedCollection) Invoke(nint drawObjectAddress) + => base.Invoke(drawObjectAddress); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiGameState api) + => new(pi, Label, api.GetDrawObjectInfo); +} + +/// +public sealed class GetCutsceneParentIndex(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetCutsceneParentIndex)}"; + + /// + public new int Invoke(int actorIndex) + => base.Invoke(actorIndex); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiGameState api) + => new(pi, Label, api.GetCutsceneParentIndex); +} + +/// +public sealed class SetCutsceneParentIndex(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(SetCutsceneParentIndex)}.V5"; + + /// + public new PenumbraApiEc Invoke(int copyIdx, int newParentIdx) + => (PenumbraApiEc)base.Invoke(copyIdx, newParentIdx); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiGameState api) + => new(pi, Label, (a, b) => (int) api.SetCutsceneParentIndex(a, b)); +} + +/// +public static class CreatingCharacterBase +{ + /// The label. + public const string Label = $"Penumbra.{nameof(CreatingCharacterBase)}.V5"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, + params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiGameState api) + => new(pi, Label, t => api.CreatingCharacterBase += t.Invoke, t => api.CreatingCharacterBase -= t.Invoke); +} + +/// +public static class CreatedCharacterBase +{ + /// The label. + public const string Label = $"Penumbra.{nameof(CreatedCharacterBase)}.V5"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiGameState api) + => new(pi, Label, t => api.CreatedCharacterBase += t.Invoke, t => api.CreatedCharacterBase -= t.Invoke); +} + +/// +public static class GameObjectResourcePathResolved +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GameObjectResourcePathResolved)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiGameState api) + => new(pi, Label, t => api.GameObjectResourceResolved += t.Invoke, t => api.GameObjectResourceResolved -= t.Invoke); +} diff --git a/Penumbra.Api/IpcSubscribers/Legacy/Collection.cs b/Penumbra.Api/IpcSubscribers/Legacy/Collection.cs new file mode 100644 index 0000000..824abd0 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Legacy/Collection.cs @@ -0,0 +1,99 @@ +using Dalamud.Plugin; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Penumbra.Api.IpcSubscribers.Legacy; + +public sealed class GetCollections(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetCollections)}"; + + public new IList Invoke() + => base.Invoke(); +} + +public sealed class GetCurrentCollectionName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetCurrentCollectionName)}"; + + public new string Invoke() + => base.Invoke(); +} + +public sealed class GetDefaultCollectionName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetDefaultCollectionName)}"; + + public new string Invoke() + => base.Invoke(); +} + +public sealed class GetInterfaceCollectionName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetInterfaceCollectionName)}"; + + public new string Invoke() + => base.Invoke(); +} + +public sealed class GetCharacterCollectionName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetCharacterCollectionName)}"; + + public new (string, bool) Invoke(string characterName) + => base.Invoke(characterName); +} + +public sealed class GetChangedItems(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetChangedItems)}"; + + public new IReadOnlyDictionary Invoke(string collectionName) + => base.Invoke(collectionName); +} + +public sealed class GetCollectionForType(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetCollectionForType)}"; + + public new string Invoke(ApiCollectionType collectionType) + => base.Invoke(collectionType); +} + +public sealed class SetCollectionForType(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(SetCollectionForType)}"; + + public new (PenumbraApiEc ErrorCode, string OldCollectionName) Invoke(ApiCollectionType collectionType, string collectionName, + bool allowCreateNew = true, bool allowDelete = true) + => base.Invoke(collectionType, collectionName, allowCreateNew, allowDelete); +} + +public sealed class GetCollectionForObject(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetCollectionForObject)}"; + + public new (bool ObjectValid, bool IndividualSet, string CollectionName) Invoke(int objectIndex) + => base.Invoke(objectIndex); +} + +public sealed class SetCollectionForObject(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(SetCollectionForObject)}"; + + public new (PenumbraApiEc ErrorCode, string OldCollectionName) Invoke(int objectIndex, string collectionName, bool allowCreateNew = true, + bool allowDelete = true) + => base.Invoke(objectIndex, collectionName, allowCreateNew, allowDelete); +} diff --git a/Penumbra.Api/IpcSubscribers/Legacy/GameState.cs b/Penumbra.Api/IpcSubscribers/Legacy/GameState.cs new file mode 100644 index 0000000..7e36389 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Legacy/GameState.cs @@ -0,0 +1,43 @@ +using Dalamud.Plugin; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Penumbra.Api.IpcSubscribers.Legacy; + +public sealed class GetDrawObjectInfo(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetDrawObjectInfo)}"; + + public new (nint GameObjectAddress, string CollectionName) Invoke(nint drawObjectAddress) + => base.Invoke(drawObjectAddress); +} + +public sealed class SetCutsceneParentIndex(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(SetCutsceneParentIndex)}"; + + public new PenumbraApiEc Invoke(int cutsceneObjectIndex, int newParentIndex) + => base.Invoke(cutsceneObjectIndex, newParentIndex); +} + +public static class CreatingCharacterBase +{ + public const string Label = $"Penumbra.{nameof(CreatingCharacterBase)}"; + + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, + params Action[] actions) + => new(pi, Label, actions); +} + +public static class CreatedCharacterBase +{ + public const string Label = $"Penumbra.{nameof(CreatedCharacterBase)}"; + + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, + params Action[] actions) + => new(pi, Label, actions); +} diff --git a/Penumbra.Api/IpcSubscribers/Legacy/Meta.cs b/Penumbra.Api/IpcSubscribers/Legacy/Meta.cs new file mode 100644 index 0000000..bed77d3 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Legacy/Meta.cs @@ -0,0 +1,24 @@ +using Dalamud.Plugin; +using Penumbra.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Penumbra.Api.IpcSubscribers.Legacy; + +public sealed class GetMetaManipulations(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetMetaManipulations)}"; + + public new string Invoke(string objectName) + => base.Invoke(objectName); +} + +public sealed class GetGameObjectMetaManipulations(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetGameObjectMetaManipulations)}"; + + public new string Invoke(int objectIndex) + => base.Invoke(objectIndex); +} diff --git a/Penumbra.Api/IpcSubscribers/Legacy/ModSettings.cs b/Penumbra.Api/IpcSubscribers/Legacy/ModSettings.cs new file mode 100644 index 0000000..a36092e --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Legacy/ModSettings.cs @@ -0,0 +1,92 @@ +using Dalamud.Plugin; +using Penumbra.Api.Api; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Penumbra.Api.IpcSubscribers.Legacy; + +using CurrentSettings = ValueTuple>, bool)?>; + +public sealed class GetAvailableModSettings(IDalamudPluginInterface pi) + : FuncSubscriber, GroupType)>?>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetAvailableModSettings)}"; + + public new IDictionary, GroupType)>? Invoke(string modDirectory, string modName = "") + => base.Invoke(modDirectory, modName); +} + +public sealed class GetCurrentModSettings(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetCurrentModSettings)}"; + + public new CurrentSettings Invoke(string collectionName, string modDirectory, string modName = "", bool ignoreInheritance = false) + => base.Invoke(collectionName, modDirectory, modName, ignoreInheritance); +} + +public sealed class TryInheritMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(TryInheritMod)}"; + + public PenumbraApiEc Invoke(string collectionName, string modDirectory, bool inherit, string modName = "") + => Invoke(collectionName, modDirectory, modName, inherit); +} + +public sealed class TrySetMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(TrySetMod)}"; + + public PenumbraApiEc Invoke(string collectionName, string modDirectory, bool enabled, string modName = "") + => Invoke(collectionName, modDirectory, modName, enabled); +} + +public sealed class TrySetModPriority(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(TrySetModPriority)}"; + + public PenumbraApiEc Invoke(string collectionName, string modDirectory, int priority, string modName = "") + => Invoke(collectionName, modDirectory, modName, priority); +} + +public sealed class TrySetModSetting(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(TrySetModSetting)}"; + + public new PenumbraApiEc Invoke(string collectionName, string modDirectory, string groupName, string setting, string modName = "") + => base.Invoke(collectionName, modDirectory, modName, groupName, setting); +} + +public sealed class TrySetModSettings(IDalamudPluginInterface pi) + : FuncSubscriber, PenumbraApiEc>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(TrySetModSettings)}"; + + public PenumbraApiEc Invoke(string collectionName, string modDirectory, string groupName, IReadOnlyList settings, + string modName = "") + => Invoke(collectionName, modDirectory, modName, groupName, settings); +} + +public static class ModSettingChanged +{ + public const string Label = $"Penumbra.{nameof(ModSettingChanged)}.V5"; + + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, + params Action[] actions) + => new(pi, Label, actions); +} + +public sealed class CopyModSettings(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(CopyModSettings)}"; + + public new PenumbraApiEc Invoke(string collectionName, string modDirectoryFrom, string modDirectoryTo) + => base.Invoke(collectionName, modDirectoryFrom, modDirectoryTo); +} diff --git a/Penumbra.Api/IpcSubscribers/Legacy/Mods.cs b/Penumbra.Api/IpcSubscribers/Legacy/Mods.cs new file mode 100644 index 0000000..e94e032 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Legacy/Mods.cs @@ -0,0 +1,70 @@ +using Dalamud.Plugin; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Penumbra.Api.IpcSubscribers.Legacy; + +public sealed class GetMods(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetMods)}"; + + public new IList<(string, string)> Invoke() + => base.Invoke(); +} + +public sealed class ReloadMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(ReloadMod)}"; + + public new PenumbraApiEc Invoke(string modDirectory, string modName = "") + => base.Invoke(modDirectory, modName); +} + +public sealed class InstallMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(InstallMod)}"; + + public new PenumbraApiEc Invoke(string modDirectory) + => base.Invoke(modDirectory); +} + +public sealed class AddMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(AddMod)}"; + + public new PenumbraApiEc Invoke(string modDirectory) + => base.Invoke(modDirectory); +} + +public sealed class DeleteMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(DeleteMod)}"; + + public new PenumbraApiEc Invoke(string modDirectory, string modName = "") + => base.Invoke(modDirectory, modName); +} + +public sealed class GetModPath(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetModPath)}"; + + public new (PenumbraApiEc ErrorCode, string Path, bool IsDefault) Invoke(string modDirectory, string modName = "") + => base.Invoke(modDirectory, modName); +} + +public sealed class SetModPath(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(SetModPath)}"; + + public new PenumbraApiEc Invoke(string modDirectory, string newPath, string modName = "") + => base.Invoke(modDirectory, modName, newPath); +} diff --git a/Penumbra.Api/IpcSubscribers/Legacy/PluginState.cs b/Penumbra.Api/IpcSubscribers/Legacy/PluginState.cs new file mode 100644 index 0000000..5107ddd --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Legacy/PluginState.cs @@ -0,0 +1,15 @@ +using Dalamud.Plugin; +using Penumbra.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Penumbra.Api.IpcSubscribers.Legacy; + +public class ApiVersions(IDalamudPluginInterface pi) + : FuncSubscriber<(int Breaking, int Features)>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(ApiVersions)}"; + + public new (int Breaking, int Features) Invoke() + => base.Invoke(); +} diff --git a/Penumbra.Api/IpcSubscribers/Legacy/Redraw.cs b/Penumbra.Api/IpcSubscribers/Legacy/Redraw.cs new file mode 100644 index 0000000..ba42cbf --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Legacy/Redraw.cs @@ -0,0 +1,44 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Penumbra.Api.IpcSubscribers.Legacy; + +public sealed class RedrawAll(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(RedrawAll)}"; + + public new void Invoke(RedrawType type) + => base.Invoke(type); +} + +public sealed class RedrawObject(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(RedrawObject)}"; + + public new void Invoke(IGameObject gameObject, RedrawType type = RedrawType.Redraw) + => base.Invoke(gameObject, type); +} + +public sealed class RedrawObjectByIndex(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(RedrawObjectByIndex)}"; + + public new void Invoke(int gameObjectIndex, RedrawType type = RedrawType.Redraw) + => base.Invoke(gameObjectIndex, type); +} + +public sealed class RedrawObjectByName(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(RedrawObjectByName)}"; + + public new void Invoke(string gameObjectName, RedrawType type = RedrawType.Redraw) + => base.Invoke(gameObjectName, type); +} diff --git a/Penumbra.Api/IpcSubscribers/Legacy/Resolve.cs b/Penumbra.Api/IpcSubscribers/Legacy/Resolve.cs new file mode 100644 index 0000000..598bbf7 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Legacy/Resolve.cs @@ -0,0 +1,24 @@ +using Dalamud.Plugin; +using Penumbra.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Penumbra.Api.IpcSubscribers.Legacy; + +public sealed class ResolveCharacterPath(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(ResolveCharacterPath)}"; + + public new string Invoke(string gamePath, string characterName) + => base.Invoke(gamePath, characterName); +} + +public sealed class ReverseResolvePath(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(ReverseResolvePath)}"; + + public new string Invoke(string gamePath, string characterName) + => base.Invoke(gamePath, characterName); +} diff --git a/Penumbra.Api/IpcSubscribers/Legacy/ResourceTree.cs b/Penumbra.Api/IpcSubscribers/Legacy/ResourceTree.cs new file mode 100644 index 0000000..255d58c --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Legacy/ResourceTree.cs @@ -0,0 +1,83 @@ +using Dalamud.Plugin; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using ResourceType = Penumbra.Api.Enums.ResourceType; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Penumbra.Api.IpcSubscribers.Legacy; + +public sealed class GetGameObjectResourcePaths(IDalamudPluginInterface pi) + : FuncSubscriber?[]>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetGameObjectResourcePaths)}"; + + public new IReadOnlyDictionary?[] Invoke(params ushort[] objectIndices) + => base.Invoke(objectIndices); +} + +public sealed class GetPlayerResourcePaths(IDalamudPluginInterface pi) + : FuncSubscriber>>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetPlayerResourcePaths)}"; + + public new IReadOnlyDictionary> Invoke() + => base.Invoke(); +} + +public sealed class GetGameObjectResourcesOfType(IDalamudPluginInterface pi) + : FuncSubscriber?[]>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetGameObjectResourcesOfType)}"; + + public new IReadOnlyDictionary?[] Invoke(ResourceType type, bool withUiData = false, + params ushort[] indices) + => base.Invoke(type, withUiData, indices); +} + +public sealed class GetPlayerResourcesOfType(IDalamudPluginInterface pi) + : FuncSubscriber>>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetPlayerResourcesOfType)}"; + + public new IReadOnlyDictionary> Invoke(ResourceType type, + bool withUiData = false) + => base.Invoke(type, withUiData); +} + +public sealed class GetGameObjectResourceTrees(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetGameObjectResourceTrees)}"; + + public new ResourceTree?[] Invoke(bool withUiData = false, params ushort[] indices) + => base.Invoke(withUiData, indices); +} + +public sealed class GetPlayerResourceTrees(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetPlayerResourceTrees)}"; + + public new IReadOnlyDictionary Invoke(bool withUiData = false) + => base.Invoke(withUiData); +} + +public record ResourceTree +{ + public required string Name { get; init; } + public required ushort RaceCode { get; init; } + public required List Nodes { get; init; } +} + +public record ResourceNode +{ + public required ResourceType Type { get; init; } + public required ChangedItemIcon Icon { get; init; } + public required string? Name { get; init; } + public required string? GamePath { get; init; } + public required string ActualPath { get; init; } + public required nint ObjectAddress { get; init; } + public required nint ResourceHandle { get; init; } + public required List Children { get; init; } +} diff --git a/Penumbra.Api/IpcSubscribers/Legacy/Temporary.cs b/Penumbra.Api/IpcSubscribers/Legacy/Temporary.cs new file mode 100644 index 0000000..4bc27e7 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Legacy/Temporary.cs @@ -0,0 +1,88 @@ +using Dalamud.Plugin; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Penumbra.Api.IpcSubscribers.Legacy; + +public sealed class CreateTemporaryCollection(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(CreateTemporaryCollection)}"; + + public new (PenumbraApiEc ErrorCode, string CollectionName) Invoke(string tag, string character, bool forceOverwrite) + => base.Invoke(tag, character, forceOverwrite); +} + +public sealed class RemoveTemporaryCollection(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(RemoveTemporaryCollection)}"; + + public new PenumbraApiEc Invoke(string collectionName) + => base.Invoke(collectionName); +} + +public sealed class CreateNamedTemporaryCollection(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(CreateNamedTemporaryCollection)}"; + + public new PenumbraApiEc Invoke(string collectionName) + => base.Invoke(collectionName); +} + +public sealed class RemoveTemporaryCollectionByName(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(RemoveTemporaryCollectionByName)}"; + + public new PenumbraApiEc Invoke(string collectionName) + => base.Invoke(collectionName); +} + +public sealed class AssignTemporaryCollection(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(AssignTemporaryCollection)}"; + + public new PenumbraApiEc Invoke(string collectionName, int gameObjectIndex, bool force) + => base.Invoke(collectionName, gameObjectIndex, force); +} + +public sealed class AddTemporaryModAll(IDalamudPluginInterface pi) + : FuncSubscriber, string, int, PenumbraApiEc>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(AddTemporaryModAll)}"; + + public new PenumbraApiEc Invoke(string tag, Dictionary files, string meta, int priority = 0) + => base.Invoke(tag, files, meta, priority); +} + +public sealed class AddTemporaryMod(IDalamudPluginInterface pi) + : FuncSubscriber, string, int, PenumbraApiEc>(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(AddTemporaryMod)}"; + + public new PenumbraApiEc Invoke(string tag, string collectionName, Dictionary files, string meta, int priority = 0) + => base.Invoke(tag, collectionName, files, meta, priority); +} + +public sealed class RemoveTemporaryModAll(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(RemoveTemporaryModAll)}"; + + public new PenumbraApiEc Invoke(string tag, int priority = 0) + => base.Invoke(tag, priority); +} + +public sealed class RemoveTemporaryMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(RemoveTemporaryMod)}"; + + public new PenumbraApiEc Invoke(string tag, string collectionName, int priority = 0) + => base.Invoke(tag, collectionName, priority); +} diff --git a/Penumbra.Api/IpcSubscribers/Legacy/Ui.cs b/Penumbra.Api/IpcSubscribers/Legacy/Ui.cs new file mode 100644 index 0000000..34ee7b4 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Legacy/Ui.cs @@ -0,0 +1,16 @@ +using Dalamud.Plugin; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Penumbra.Api.IpcSubscribers.Legacy; + +public sealed class OpenMainWindow(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(OpenMainWindow)}"; + + public new PenumbraApiEc Invoke(TabType tab, string modName, string modDirectory = "") + => base.Invoke(tab, modName, modDirectory); +} diff --git a/Penumbra.Api/IpcSubscribers/Meta.cs b/Penumbra.Api/IpcSubscribers/Meta.cs new file mode 100644 index 0000000..7cdea16 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Meta.cs @@ -0,0 +1,37 @@ +using Dalamud.Plugin; +using Penumbra.Api.Api; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.IpcSubscribers; + +/// +public sealed class GetPlayerMetaManipulations(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetPlayerMetaManipulations)}"; + + /// + public new string Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiMeta api) + => new(pi, Label, api.GetPlayerMetaManipulations); +} + +/// +public sealed class GetMetaManipulations(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetMetaManipulations)}.V5"; + + /// + public new string Invoke(int gameObjectIdx) + => base.Invoke(gameObjectIdx); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiMeta api) + => new(pi, Label, api.GetMetaManipulations); +} diff --git a/Penumbra.Api/IpcSubscribers/ModSettings.cs b/Penumbra.Api/IpcSubscribers/ModSettings.cs new file mode 100644 index 0000000..16422cb --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/ModSettings.cs @@ -0,0 +1,218 @@ +using Dalamud.Plugin; +using Penumbra.Api.Api; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.IpcSubscribers; + +using CurrentSettingsBase = ValueTuple>, bool)?>; +using CurrentSettings = ValueTuple>, bool)?>; +using CurrentSettingsTempBase = ValueTuple>, bool, bool)?>; +using CurrentSettingsTemp = ValueTuple>, bool, bool)?>; + +/// +public sealed class GetAvailableModSettings(IDalamudPluginInterface pi) + : FuncSubscriber?>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetAvailableModSettings)}.V5"; + + /// + public new IReadOnlyDictionary? Invoke(string modDirectory, string modName = "") + => AvailableModSettings.Create(base.Invoke(modDirectory, modName)); + + /// Create a provider. + public static FuncProvider?> Provider(IDalamudPluginInterface pi, + IPenumbraApiModSettings api) + => new(pi, Label, (a, b) => api.GetAvailableModSettings(a, b)?.Original); +} + +/// +public sealed class GetCurrentModSettings(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetCurrentModSettings)}.V5"; + + /// + public new CurrentSettings Invoke(Guid collectionId, string modDirectory, string modName = "", bool ignoreInheritance = false) + { + var (ret, t) = base.Invoke(collectionId, modDirectory, modName, ignoreInheritance); + return ((PenumbraApiEc)ret, t); + } + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, + IPenumbraApiModSettings api) + => new(pi, Label, (a, b, c, d) => + { + var (ret, t) = api.GetCurrentModSettings(a, b, c, d); + return ((int)ret, t); + }); +} + +/// +public sealed class GetCurrentModSettingsWithTemp(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetCurrentModSettingsWithTemp)}"; + + /// + public new CurrentSettingsTemp Invoke(Guid collectionId, string modDirectory, string modName = "", bool ignoreInheritance = false, + bool ignoreTemporary = false, int key = 0) + { + var (ret, t) = base.Invoke(collectionId, modDirectory, modName, ignoreInheritance, ignoreTemporary, key); + return ((PenumbraApiEc)ret, t); + } + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, + IPenumbraApiModSettings api) + => new(pi, Label, (a, b, c, d, e, f) => + { + var (ret, t) = api.GetCurrentModSettingsWithTemp(a, b, c, d, e, f); + return ((int)ret, t); + }); +} + +/// +public sealed class GetAllModSettings(IDalamudPluginInterface pi) + : FuncSubscriber>, bool, bool)>?)>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetAllModSettings)}"; + + /// + public new (PenumbraApiEc, Dictionary>, bool, bool)>?) Invoke(Guid collectionId, + bool ignoreInheritance = false, bool ignoreTemporary = false, int key = 0) + { + var (ret, t) = base.Invoke(collectionId, ignoreInheritance, ignoreTemporary, key); + return ((PenumbraApiEc)ret, t); + } + + /// Create a provider. + public static FuncProvider>, bool, bool)>?)> + Provider(IDalamudPluginInterface pi, + IPenumbraApiModSettings api) + => new(pi, Label, (a, b, c, d) => + { + var (ret, t) = api.GetAllModSettings(a, b, c, d); + return ((int)ret, t); + }); +} + +/// +public sealed class TryInheritMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(TryInheritMod)}.V5"; + + /// + public PenumbraApiEc Invoke(Guid collectionId, string modDirectory, bool inherit, string modName = "") + => (PenumbraApiEc)Invoke(collectionId, modDirectory, modName, inherit); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiModSettings api) + => new(pi, Label, (a, b, c, d) => (int)api.TryInheritMod(a, b, c, d)); +} + +/// +public sealed class TrySetMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(TrySetMod)}.V5"; + + /// + public PenumbraApiEc Invoke(Guid collectionId, string modDirectory, bool inherit, string modName = "") + => (PenumbraApiEc)Invoke(collectionId, modDirectory, modName, inherit); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiModSettings api) + => new(pi, Label, (a, b, c, d) => (int)api.TrySetMod(a, b, c, d)); +} + +/// +public sealed class TrySetModPriority(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(TrySetModPriority)}.V5"; + + /// + public PenumbraApiEc Invoke(Guid collectionId, string modDirectory, int priority, string modName = "") + => (PenumbraApiEc)Invoke(collectionId, modDirectory, modName, priority); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiModSettings api) + => new(pi, Label, (a, b, c, d) => (int)api.TrySetModPriority(a, b, c, d)); +} + +/// +public sealed class TrySetModSetting(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(TrySetModSetting)}.V5"; + + /// + public new PenumbraApiEc Invoke(Guid collectionId, string modDirectory, string optionGroupName, string optionName, string modName = "") + => (PenumbraApiEc)base.Invoke(collectionId, modDirectory, modName, optionGroupName, optionName); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiModSettings api) + => new(pi, Label, (a, b, c, d, e) => (int)api.TrySetModSetting(a, b, c, d, e)); +} + +/// +public sealed class TrySetModSettings(IDalamudPluginInterface pi) + : FuncSubscriber, int>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(TrySetModSettings)}.V5"; + + /// + public PenumbraApiEc Invoke(Guid collectionId, string modDirectory, string optionGroupName, + IReadOnlyList optionNames, string modName = "") + => (PenumbraApiEc)Invoke(collectionId, modDirectory, modName, optionGroupName, optionNames); + + /// Create a provider. + public static FuncProvider, int> Provider(IDalamudPluginInterface pi, + IPenumbraApiModSettings api) + => new(pi, Label, (a, b, c, d, e) => (int)api.TrySetModSettings(a, b, c, d, e)); +} + +/// +public static class ModSettingChanged +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ModSettingChanged)}.V5"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, + params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiModSettings api) + => new(pi, Label, t => api.ModSettingChanged += t.Invoke, t => api.ModSettingChanged -= t.Invoke); +} + +/// +public sealed class CopyModSettings(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(CopyModSettings)}.V5"; + + /// + public new PenumbraApiEc Invoke(Guid? collectionId, string modDirectoryFrom, string modDirectoryTo) + => (PenumbraApiEc)base.Invoke(collectionId, modDirectoryFrom, modDirectoryTo); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, + IPenumbraApiModSettings api) + => new(pi, Label, (a, b, c) => (int)api.CopyModSettings(a, b, c)); +} diff --git a/Penumbra.Api/IpcSubscribers/Mods.cs b/Penumbra.Api/IpcSubscribers/Mods.cs new file mode 100644 index 0000000..5d1c953 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Mods.cs @@ -0,0 +1,218 @@ +using Dalamud.Plugin; +using Penumbra.Api.Api; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.IpcSubscribers; + +/// +public sealed class GetModList(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetModList)}"; + + /// + public new Dictionary Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider> Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, api.GetModList); +} + +/// +public sealed class InstallMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(InstallMod)}.V5"; + + /// + public new PenumbraApiEc Invoke(string modFilePackagePath) + => (PenumbraApiEc)base.Invoke(modFilePackagePath); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, a => (int)api.InstallMod(a)); +} + +/// +public sealed class ReloadMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ReloadMod)}.V5"; + + /// + public new PenumbraApiEc Invoke(string modDirectory, string modName = "") + => (PenumbraApiEc)base.Invoke(modDirectory, modName); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, (a, b) => (int)api.ReloadMod(a, b)); +} + +/// +public sealed class AddMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(AddMod)}.V5"; + + /// + public new PenumbraApiEc Invoke(string modDirectory) + => (PenumbraApiEc)base.Invoke(modDirectory); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, a => (int)api.AddMod(a)); +} + +/// +public sealed class DeleteMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(DeleteMod)}.V5"; + + /// + public new PenumbraApiEc Invoke(string modDirectory, string modName = "") + => (PenumbraApiEc)base.Invoke(modDirectory, modName); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, (a, b) => (int)api.DeleteMod(a, b)); +} + +/// +public static class ModDeleted +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ModDeleted)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, (t => api.ModDeleted += t, t => api.ModDeleted -= t)); +} + +/// +public static class ModAdded +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ModAdded)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, (t => api.ModAdded += t, t => api.ModAdded -= t)); +} + +/// +public static class ModMoved +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ModMoved)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, (t => api.ModMoved += t, t => api.ModMoved -= t)); +} + +/// +public sealed class GetModPath(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetModPath)}.V5"; + + /// + public new (PenumbraApiEc, string FullPath, bool FullDefault, bool NameDefault) Invoke(string modDirectory, string modName = "") + { + var (ret, fullPath, fullDefault, nameDefault) = base.Invoke(modDirectory, modName); + return ((PenumbraApiEc)ret, fullPath, fullDefault, nameDefault); + } + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, (a, b) => + { + var (ret, fullPath, fullDefault, nameDefault) = api.GetModPath(a, b); + return ((int)ret, fullPath, fullDefault, nameDefault); + }); +} + +/// +public sealed class SetModPath(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(SetModPath)}.V5"; + + /// + public new PenumbraApiEc Invoke(string modDirectory, string newPath, string modName = "") + => (PenumbraApiEc)base.Invoke(modDirectory, modName, newPath); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, (a, b, c) => (int)api.SetModPath(a, b, c)); +} + +/// +public sealed class GetChangedItems(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetChangedItems)}.V5"; + + /// + public new Dictionary Invoke(string modDirectory, string modName) + => base.Invoke(modDirectory, modName); + + /// Create a provider. + public static FuncProvider> Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, api.GetChangedItems); +} + +/// +public sealed class GetChangedItemAdapterDictionary(IDalamudPluginInterface pi) + : FuncSubscriber>>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetChangedItemAdapterDictionary)}"; + + /// + public new IReadOnlyDictionary> Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider>> Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, api.GetChangedItemAdapterDictionary); +} + +/// +public sealed class GetChangedItemAdapterList(IDalamudPluginInterface pi) + : FuncSubscriber ChangedItems)>>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetChangedItemAdapterList)}"; + + /// + public new IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)> Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider ChangedItems)>> Provider(IDalamudPluginInterface pi, IPenumbraApiMods api) + => new(pi, Label, api.GetChangedItemAdapterList); +} diff --git a/Penumbra.Api/IpcSubscribers/PluginState.cs b/Penumbra.Api/IpcSubscribers/PluginState.cs new file mode 100644 index 0000000..fb5d695 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/PluginState.cs @@ -0,0 +1,128 @@ +using Dalamud.Plugin; +using Penumbra.Api.Api; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.IpcSubscribers; + +/// Triggered when the Penumbra API is initialized and ready. +public static class Initialized +{ + /// The label. + public const string Label = $"Penumbra.{nameof(Initialized)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi) + => new(pi, Label); +} + +/// Triggered when the Penumbra API is fully disposed and unavailable. +public static class Disposed +{ + /// The label. + public const string Label = $"Penumbra.{nameof(Disposed)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi) + => new(pi, Label); +} + +/// +public class ApiVersion(IDalamudPluginInterface pi) + : FuncSubscriber<(int Breaking, int Features)>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ApiVersion)}.V5"; + + /// + public new (int Breaking, int Features) Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider<(int Breaking, int Features)> Provider(IDalamudPluginInterface pi, IPenumbraApiBase api) + => new(pi, Label, () => api.ApiVersion); +} + +/// +public class GetModDirectory(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetModDirectory)}"; + + /// + public new string Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiPluginState api) + => new(pi, Label, api.GetModDirectory); +} + +/// +public class GetConfiguration(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetConfiguration)}"; + + /// + public new string Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiPluginState api) + => new(pi, Label, api.GetConfiguration); +} + +/// +public static class ModDirectoryChanged +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ModDirectoryChanged)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiPluginState api) + => new(pi, Label, (t => api.ModDirectoryChanged += t, t => api.ModDirectoryChanged -= t)); +} + +/// +public class GetEnabledState(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + public const string Label = $"Penumbra.{nameof(GetEnabledState)}"; + + /// + public new bool Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiPluginState api) + => new(pi, Label, api.GetEnabledState); +} + +/// +public static class EnabledChange +{ + /// The label. + public const string Label = $"Penumbra.{nameof(EnabledChange)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiPluginState api) + => new(pi, Label, (t => api.EnabledChange += t, t => api.EnabledChange -= t)); +} diff --git a/Penumbra.Api/IpcSubscribers/Redraw.cs b/Penumbra.Api/IpcSubscribers/Redraw.cs new file mode 100644 index 0000000..2a18760 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Redraw.cs @@ -0,0 +1,53 @@ +using Dalamud.Plugin; +using Penumbra.Api.Api; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.IpcSubscribers; + +/// +public sealed class RedrawObject(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(RedrawObject)}.V5"; + + /// + public void Invoke(int gameObjectIndex, RedrawType setting = RedrawType.Redraw) + => base.Invoke(gameObjectIndex, (int)setting); + + /// Create a provider. + public static ActionProvider Provider(IDalamudPluginInterface pi, IPenumbraApiRedraw api) + => new(pi, Label, (a, b) => api.RedrawObject(a, (RedrawType)b)); +} + +/// +public sealed class RedrawAll(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(RedrawAll)}.V5"; + + /// + public void Invoke(RedrawType setting = RedrawType.Redraw) + => base.Invoke((int)setting); + + /// Create a provider. + public static ActionProvider Provider(IDalamudPluginInterface pi, IPenumbraApiRedraw api) + => new(pi, Label, a => api.RedrawAll((RedrawType)a)); +} + +/// +public static class GameObjectRedrawn +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GameObjectRedrawn)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiRedraw api) + => new(pi, Label, t => api.GameObjectRedrawn += t.Invoke, t => api.GameObjectRedrawn -= t.Invoke); +} diff --git a/Penumbra.Api/IpcSubscribers/Resolve.cs b/Penumbra.Api/IpcSubscribers/Resolve.cs new file mode 100644 index 0000000..20a2ff9 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Resolve.cs @@ -0,0 +1,133 @@ +using Dalamud.Plugin; +using Penumbra.Api.Api; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.IpcSubscribers; + +/// +public sealed class ResolveDefaultPath(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ResolveDefaultPath)}"; + + /// + public new string Invoke(string gamePath) + => base.Invoke(gamePath); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiResolve api) + => new(pi, Label, api.ResolveDefaultPath); +} + +/// +public sealed class ResolveInterfacePath(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ResolveInterfacePath)}"; + + /// + public new string Invoke(string gamePath) + => base.Invoke(gamePath); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiResolve api) + => new(pi, Label, api.ResolveInterfacePath); +} + +/// +public sealed class ResolveGameObjectPath(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ResolveGameObjectPath)}"; + + /// + public new string Invoke(string gamePath, int gameObjectIdx) + => base.Invoke(gamePath, gameObjectIdx); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiResolve api) + => new(pi, Label, api.ResolveGameObjectPath); +} + +/// +public sealed class ResolvePlayerPath(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ResolvePlayerPath)}"; + + /// + public new string Invoke(string gamePath) + => base.Invoke(gamePath); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiResolve api) + => new(pi, Label, api.ResolvePlayerPath); +} + +/// +public sealed class ReverseResolveGameObjectPath(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ReverseResolveGameObjectPath)}"; + + /// + public new string[] Invoke(string gamePath, int gameObjectIdx) + => base.Invoke(gamePath, gameObjectIdx); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiResolve api) + => new(pi, Label, api.ReverseResolveGameObjectPath); +} + +/// +public sealed class ReverseResolvePlayerPath(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ReverseResolvePlayerPath)}"; + + /// + public new string[] Invoke(string gamePath) + => base.Invoke(gamePath); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiResolve api) + => new(pi, Label, api.ReverseResolvePlayerPath); +} + +/// +public sealed class ResolvePlayerPaths(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ResolvePlayerPaths)}"; + + /// + public new (string[], string[][]) Invoke(string[] forward, string[] reverse) + => base.Invoke(forward, reverse); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiResolve api) + => new(pi, Label, api.ResolvePlayerPaths); +} + +/// +public sealed class ResolvePlayerPathsAsync(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ResolvePlayerPathsAsync)}"; + + /// + public new Task<(string[], string[][])> Invoke(string[] forward, string[] reverse) + => base.Invoke(forward, reverse); + + /// Create a provider. + public static FuncProvider> Provider(IDalamudPluginInterface pi, IPenumbraApiResolve api) + => new(pi, Label, api.ResolvePlayerPathsAsync); +} diff --git a/Penumbra.Api/IpcSubscribers/ResourceTree.cs b/Penumbra.Api/IpcSubscribers/ResourceTree.cs new file mode 100644 index 0000000..28263b3 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/ResourceTree.cs @@ -0,0 +1,116 @@ +using System.Linq; +using Dalamud.Plugin; +using Newtonsoft.Json.Linq; +using Penumbra.Api.Api; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.IpcSubscribers; + +/// +public sealed class GetGameObjectResourcePaths(IDalamudPluginInterface pi) + : FuncSubscriber>?[]>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetGameObjectResourcePaths)}.V5"; + + /// + public new Dictionary>?[] Invoke(params ushort[] gameObjectIndices) + => base.Invoke(gameObjectIndices); + + /// Create a provider. + public static FuncProvider>?[]> Provider(IDalamudPluginInterface pi, + IPenumbraApiResourceTree api) + => new(pi, Label, api.GetGameObjectResourcePaths); +} + +/// +public sealed class GetPlayerResourcePaths(IDalamudPluginInterface pi) + : FuncSubscriber>>>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetPlayerResourcePaths)}.V5"; + + /// + public new Dictionary>> Invoke() + => base.Invoke(); + + /// Create a provider. + public static FuncProvider>>> Provider(IDalamudPluginInterface pi, + IPenumbraApiResourceTree api) + => new(pi, Label, api.GetPlayerResourcePaths); +} + +/// +public sealed class GetGameObjectResourcesOfType(IDalamudPluginInterface pi) + : FuncSubscriber?[]>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetGameObjectResourcesOfType)}.V5"; + + /// + public IReadOnlyDictionary?[] Invoke(ResourceType type, bool withUiData = false, + params ushort[] gameObjectIndices) + => Array.ConvertAll(Invoke((uint)type, withUiData, gameObjectIndices), + d => (IReadOnlyDictionary?)GameResourceDict.Create(d)); + + /// Create a provider. + public static FuncProvider?[]> Provider(IDalamudPluginInterface pi, + IPenumbraApiResourceTree api) + => new(pi, Label, + (a, b, c) => Array.ConvertAll(api.GetGameObjectResourcesOfType((ResourceType)a, b, c), d => d?.Original)); +} + +/// +public sealed class GetPlayerResourcesOfType(IDalamudPluginInterface pi) + : FuncSubscriber>>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetPlayerResourcesOfType)}.V5"; + + /// + public Dictionary> Invoke(ResourceType type, bool withUiData = false) + => Invoke((uint)type, withUiData) + .ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyDictionary)new GameResourceDict(kvp.Value)); + + /// Create a provider. + public static FuncProvider>> Provider( + IDalamudPluginInterface pi, + IPenumbraApiResourceTree api) + => new(pi, Label, + (a, b) => api.GetPlayerResourcesOfType((ResourceType)a, b) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Original)); +} + +/// +public sealed class GetGameObjectResourceTrees(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetGameObjectResourceTrees)}.V5"; + + /// + public new ResourceTreeDto?[] Invoke(bool withUiData = false, params ushort[] gameObjectIndices) + => Array.ConvertAll(base.Invoke(withUiData, gameObjectIndices), o => o?.ToObject()); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, + IPenumbraApiResourceTree api) + => new(pi, Label, api.GetGameObjectResourceTrees); +} + +/// +public sealed class GetPlayerResourceTrees(IDalamudPluginInterface pi) + : FuncSubscriber>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(GetPlayerResourceTrees)}.V5"; + + /// + public new Dictionary Invoke(bool withUiData = false) + => base.Invoke(withUiData).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToObject()!); + + /// Create a provider. + public static FuncProvider> Provider(IDalamudPluginInterface pi, IPenumbraApiResourceTree api) + => new(pi, Label, api.GetPlayerResourceTrees); +} diff --git a/Penumbra.Api/IpcSubscribers/Temporary.cs b/Penumbra.Api/IpcSubscribers/Temporary.cs new file mode 100644 index 0000000..24aab87 --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Temporary.cs @@ -0,0 +1,276 @@ +using Dalamud.Plugin; +using Penumbra.Api.Api; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using PseudoModSetting = + System.ValueTuple>>; + +namespace Penumbra.Api.IpcSubscribers; + +/// +public sealed class CreateTemporaryCollection(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(CreateTemporaryCollection)}.V5"; + + /// + public new Guid Invoke(string name = "") + => base.Invoke(name); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiTemporary api) + => new(pi, Label, api.CreateTemporaryCollection); +} + +/// +public sealed class DeleteTemporaryCollection(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(DeleteTemporaryCollection)}.V5"; + + /// + public new PenumbraApiEc Invoke(Guid collectionId) + => (PenumbraApiEc)base.Invoke(collectionId); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiTemporary api) + => new(pi, Label, g => (int)api.DeleteTemporaryCollection(g)); +} + +/// +public sealed class AssignTemporaryCollection(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(AssignTemporaryCollection)}.V5"; + + /// + public new PenumbraApiEc Invoke(Guid collectionId, int actorIndex, bool forceAssignment = true) + => (PenumbraApiEc)base.Invoke(collectionId, actorIndex, forceAssignment); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiTemporary api) + => new(pi, Label, (a, b, c) => (int)api.AssignTemporaryCollection(a, b, c)); +} + +/// +public sealed class AddTemporaryModAll(IDalamudPluginInterface pi) + : FuncSubscriber, string, int, int>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(AddTemporaryModAll)}.V5"; + + /// + public new PenumbraApiEc Invoke(string tag, Dictionary paths, string manipString, int priority) + => (PenumbraApiEc)base.Invoke(tag, paths, manipString, priority); + + /// Create a provider. + public static FuncProvider, string, int, int> Provider(IDalamudPluginInterface pi, + IPenumbraApiTemporary api) + => new(pi, Label, (a, b, c, d) => (int)api.AddTemporaryModAll(a, b, c, d)); +} + +/// +public sealed class AddTemporaryMod(IDalamudPluginInterface pi) + : FuncSubscriber, string, int, int>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(AddTemporaryMod)}.V5"; + + /// + public new PenumbraApiEc Invoke(string tag, Guid collectionId, Dictionary paths, string manipString, int priority) + => (PenumbraApiEc)base.Invoke(tag, collectionId, paths, manipString, priority); + + /// Create a provider. + public static FuncProvider, string, int, int> Provider(IDalamudPluginInterface pi, + IPenumbraApiTemporary api) + => new(pi, Label, (a, b, c, d, e) => (int)api.AddTemporaryMod(a, b, c, d, e)); +} + +/// +public sealed class RemoveTemporaryModAll(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(RemoveTemporaryModAll)}.V5"; + + /// + public new PenumbraApiEc Invoke(string tag, int priority) + => (PenumbraApiEc)base.Invoke(tag, priority); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, + IPenumbraApiTemporary api) + => new(pi, Label, (a, b) => (int)api.RemoveTemporaryModAll(a, b)); +} + +/// +public sealed class RemoveTemporaryMod(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(RemoveTemporaryMod)}.V5"; + + /// + public new PenumbraApiEc Invoke(string tag, Guid collectionId, int priority) + => (PenumbraApiEc)base.Invoke(tag, collectionId, priority); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiTemporary api) + => new(pi, Label, (a, b, c) => (int)api.RemoveTemporaryMod(a, b, c)); +} + +/// +public sealed class SetTemporaryModSettings(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(SetTemporaryModSettings)}.V5"; + + /// + public PenumbraApiEc Invoke(Guid collectionId, string modDirectory, bool inherit, bool enabled, int priority, + IReadOnlyDictionary> settings, string source, int key = 0, string modName = "") + => (PenumbraApiEc)Invoke(collectionId, modDirectory, modName, (inherit, enabled, priority, settings), source, key); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, + IPenumbraApiTemporary api) + => new(pi, Label, (a, b, c, d, e, f) => (int)api.SetTemporaryModSettings(a, b, c, d.Item1, d.Item2, d.Item3, d.Item4, e, f)); +} + +/// +public sealed class SetTemporaryModSettingsPlayer(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(SetTemporaryModSettingsPlayer)}.V5"; + + /// + public PenumbraApiEc Invoke(int objectIndex, string modDirectory, bool inherit, bool enabled, int priority, + IReadOnlyDictionary> settings, string source, int key = 0, string modName = "") + => (PenumbraApiEc)Invoke(objectIndex, modDirectory, modName, (inherit, enabled, priority, settings), source, key); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, + IPenumbraApiTemporary api) + => new(pi, Label, (a, b, c, d, e, f) => (int)api.SetTemporaryModSettingsPlayer(a, b, c, d.Item1, d.Item2, d.Item3, d.Item4, e, f)); +} + +/// +public sealed class RemoveTemporaryModSettings(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(RemoveTemporaryModSettings)}.V5"; + + /// + public PenumbraApiEc Invoke(Guid collectionId, string modDirectory, int key = 0, string modName = "") + => (PenumbraApiEc)base.Invoke(collectionId, modDirectory, modName, key); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiTemporary api) + => new(pi, Label, (a, b, c, d) => (int)api.RemoveTemporaryModSettings(a, b, c, d)); +} + +/// +public sealed class RemoveTemporaryModSettingsPlayer(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(RemoveTemporaryModSettingsPlayer)}.V5"; + + /// + public PenumbraApiEc Invoke(int objectIndex, string modDirectory, int key = 0, string modName = "") + => (PenumbraApiEc)base.Invoke(objectIndex, modDirectory, modName, key); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiTemporary api) + => new(pi, Label, (a, b, c, d) => (int)api.RemoveTemporaryModSettingsPlayer(a, b, c, d)); +} + +/// +public sealed class RemoveAllTemporaryModSettings(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(RemoveAllTemporaryModSettings)}.V5"; + + /// + public new PenumbraApiEc Invoke(Guid collectionId, int key = 0) + => (PenumbraApiEc)base.Invoke(collectionId, key); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiTemporary api) + => new(pi, Label, (a, b) => (int)api.RemoveAllTemporaryModSettings(a, b)); +} + +/// +public sealed class RemoveAllTemporaryModSettingsPlayer(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(RemoveAllTemporaryModSettingsPlayer)}.V5"; + + /// + public new PenumbraApiEc Invoke(int objectIndex, int key = 0) + => (PenumbraApiEc)base.Invoke(objectIndex, key); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiTemporary api) + => new(pi, Label, (a, b) => (int)api.RemoveAllTemporaryModSettingsPlayer(a, b)); +} + +/// +public sealed class QueryTemporaryModSettings(IDalamudPluginInterface pi) + : FuncSubscriber>)?, string)>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(QueryTemporaryModSettings)}.V5"; + + /// + public PenumbraApiEc Invoke(Guid collectionId, string modDirectory, + out (bool ForceInherit, bool Enabled, int Priority, Dictionary> Settings)? settings, out string source, + int key = 0, string modName = "") + { + (var ec, settings, source) = Invoke(collectionId, modDirectory, modName, key); + return (PenumbraApiEc)ec; + } + + /// Create a provider. + public static FuncProvider>)?, string)> Provider( + IDalamudPluginInterface pi, IPenumbraApiTemporary api) + => new(pi, Label, (a, b, c, d) => + { + var (ex, settings, source) = api.QueryTemporaryModSettings(a, b, c, d); + return ((int)ex, settings, source); + }); +} + +/// +public sealed class QueryTemporaryModSettingsPlayer(IDalamudPluginInterface pi) + : FuncSubscriber>)?, string)>(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(QueryTemporaryModSettingsPlayer)}.V5"; + + /// + public PenumbraApiEc Invoke(int objectIndex, string modDirectory, + out (bool ForceInherit, bool Enabled, int Priority, Dictionary> Settings)? settings, out string source, + int key = 0, string modName = "") + { + (var ec, settings, source) = Invoke(objectIndex, modDirectory, modName, key); + return (PenumbraApiEc)ec; + } + + /// Create a provider. + public static FuncProvider>)?, string)> Provider( + IDalamudPluginInterface pi, IPenumbraApiTemporary api) + => new(pi, Label, (a, b, c, d) => + { + var (ex, settings, source) = api.QueryTemporaryModSettingsPlayer(a, b, c, d); + return ((int)ex, settings, source); + }); +} diff --git a/Penumbra.Api/IpcSubscribers/Ui.cs b/Penumbra.Api/IpcSubscribers/Ui.cs new file mode 100644 index 0000000..c20ebae --- /dev/null +++ b/Penumbra.Api/IpcSubscribers/Ui.cs @@ -0,0 +1,130 @@ +using Dalamud.Plugin; +using Penumbra.Api.Api; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api.IpcSubscribers; + +/// +public static class ChangedItemTooltip +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ChangedItemTooltip)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, + params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiUi api) + => new(pi, Label, (d => api.ChangedItemTooltip += d, d => api.ChangedItemTooltip -= d)); +} + +/// +public static class ChangedItemClicked +{ + /// The label. + public const string Label = $"Penumbra.{nameof(ChangedItemClicked)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, + params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiUi api) + => new(pi, Label, (d => api.ChangedItemClicked += d, d => api.ChangedItemClicked -= d)); +} + +/// +public static class PreSettingsTabBarDraw +{ + /// The label. + public const string Label = $"Penumbra.{nameof(PreSettingsTabBarDraw)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiUi api) + => new(pi, Label, (d => api.PreSettingsTabBarDraw += d, d => api.PreSettingsTabBarDraw -= d)); +} + +/// +public static class PreSettingsDraw +{ + /// The label. + public const string Label = $"Penumbra.{nameof(PreSettingsDraw)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiUi api) + => new(pi, Label, (d => api.PreSettingsPanelDraw += d, d => api.PreSettingsPanelDraw -= d)); +} + +/// +public static class PostEnabledDraw +{ + /// The label. + public const string Label = $"Penumbra.{nameof(PostEnabledDraw)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiUi api) + => new(pi, Label, (d => api.PostEnabledDraw += d, d => api.PostEnabledDraw -= d)); +} + +/// +public static class PostSettingsDraw +{ + /// The label. + public const string Label = $"Penumbra.{nameof(PostSettingsDraw)}"; + + /// Create a new event subscriber. + public static EventSubscriber Subscriber(IDalamudPluginInterface pi, params Action[] actions) + => new(pi, Label, actions); + + /// Create a provider. + public static EventProvider Provider(IDalamudPluginInterface pi, IPenumbraApiUi api) + => new(pi, Label, (d => api.PostSettingsPanelDraw += d, d => api.PostSettingsPanelDraw -= d)); +} + +/// +public sealed class OpenMainWindow(IDalamudPluginInterface pi) + : FuncSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra.{nameof(OpenMainWindow)}.V5"; + + /// + public PenumbraApiEc Invoke(TabType tab, string modDirectory = "", string modName = "") + => (PenumbraApiEc)Invoke((int)tab, modDirectory, modName); + + /// Create a provider. + public static FuncProvider Provider(IDalamudPluginInterface pi, IPenumbraApiUi api) + => new(pi, Label, (a, b, c) => (int)api.OpenMainWindow((TabType)a, b, c)); +} + +/// +public sealed class CloseMainWindow(IDalamudPluginInterface pi) + : ActionSubscriber(pi, Label) +{ + /// The label. + public const string Label = $"Penumbra{nameof(CloseMainWindow)}"; + + /// + public new void Invoke() + => base.Invoke(); + + /// Create a provider. + public static ActionProvider Provider(IDalamudPluginInterface pi, IPenumbraApiUi api) + => new(pi, Label, api.CloseMainWindow); +} diff --git a/Penumbra.Api/Penumbra.Api.csproj b/Penumbra.Api/Penumbra.Api.csproj new file mode 100644 index 0000000..7e5f9b1 --- /dev/null +++ b/Penumbra.Api/Penumbra.Api.csproj @@ -0,0 +1,34 @@ + + + Penumbra.Api + Penumbra + Copyright © 2025 + 5.6.1.0 + 5.6.1.0 + 5.6.1 + README.md + bin\$(Configuration)\ + + + + true + true + Penumbra.Api + Ottermandias + https://github.com/Ottermandias/Penumbra.Api + Auxiliary functions for Penumbras external API. + MIT + + + + false + + + + 1591 + + + + + + diff --git a/Penumbra.Api/Penumbra.Api.csproj.DotSettings b/Penumbra.Api/Penumbra.Api.csproj.DotSettings new file mode 100644 index 0000000..7d7508c --- /dev/null +++ b/Penumbra.Api/Penumbra.Api.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/Penumbra.Api/README.md b/Penumbra.Api/README.md new file mode 100644 index 0000000..1e9bdf1 --- /dev/null +++ b/Penumbra.Api/README.md @@ -0,0 +1,4 @@ +# Penumbra + +This is an auxiliary repository for Penumbras external API. +For more information, see the [main repo](https://github.com/xivdev/Penumbra). \ No newline at end of file diff --git a/Penumbra.Api/packages.lock.json b/Penumbra.Api/packages.lock.json new file mode 100644 index 0000000..bd07e56 --- /dev/null +++ b/Penumbra.Api/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + "net9.0-windows7.0": { + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.25, )", + "resolved": "1.2.25", + "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + } + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..edd00a2 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Snowcloak Sync +The snow may cloak the world in silence, but come in, warm up, and reveal your true colours. + +A Dalamud plugin. + +[![Discord](https://img.shields.io/discord/1408265972720078990?color=5865F2&label=discord&logo=discord&logoColor=white)](https://discord.gg/snowcloak) + +## License + +This project is based on Mare Synchronos by DarkArchon. Original code is licensed under the MIT License; see the +LICENSE_MIT file for details. Commits after 9e539810 are licenced under AGPL v3. + +The Glamourer and Penumbra APIs remain MIT licensed, and are copyright Ottermandias. \ No newline at end of file