From 4688043f6af7c1a6d732538085f3ef555564cce6 Mon Sep 17 00:00:00 2001 From: SirConstance Date: Sun, 31 Aug 2025 00:21:08 +0200 Subject: [PATCH] Initial commit --- .editorconfig | 273 +++ .gitignore | 352 +++ 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 | 360 +++ .../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 | 76 + .../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 | 65 + .../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 | 62 + .../PlayerData/Factories/PlayerDataFactory.cs | 365 ++++ .../PlayerData/Handlers/GameObjectHandler.cs | 487 +++++ .../PlayerData/Handlers/PairHandler.cs | 916 ++++++++ .../PlayerData/Pairs/OnlinePlayerManager.cs | 75 + .../PlayerData/Pairs/OptionalPluginWarning.cs | 10 + MareSynchronos/PlayerData/Pairs/Pair.cs | 376 ++++ .../PlayerData/Pairs/PairManager.cs | 403 ++++ .../Services/CacheCreationService.cs | 263 +++ MareSynchronos/Plugin.cs | 235 ++ .../CharaData/CharaDataCharacterHandler.cs | 156 ++ .../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 + MareSynchronos/Services/NoSnapService.cs | 226 ++ .../Services/NotificationService.cs | 141 ++ MareSynchronos/Services/PairAnalyzer.cs | 214 ++ .../Services/PerformanceCollectorService.cs | 199 ++ .../Services/PlayerPerformanceService.cs | 330 +++ .../PluginWarningNotificationService.cs | 76 + .../Services/PluginWatcherService.cs | 160 ++ .../Services/RemoteConfigurationService.cs | 201 ++ MareSynchronos/Services/RepoChangeConfig.cs | 12 + MareSynchronos/Services/RepoChangeService.cs | 401 ++++ .../ServerConfigurationManager.cs | 547 +++++ MareSynchronos/Services/UiFactory.cs | 60 + MareSynchronos/Services/UiService.cs | 137 ++ MareSynchronos/Services/VisibilityService.cs | 105 + MareSynchronos/Services/XivDataAnalyzer.cs | 257 +++ 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 | 369 ++++ 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 | 1025 +++++++++ MareSynchronos/UmbraSync.json | 14 + 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 | 49 + MareSynchronos/Utils/RollingList.cs | 47 + MareSynchronos/Utils/ValueProgress.cs | 22 + MareSynchronos/Utils/VariousExtensions.cs | 230 ++ .../WebAPI/AccountRegistrationService.cs | 84 + .../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 | 240 ++ .../WebAPI/SignalR/JwtIdentifier.cs | 9 + .../SignalR/MareAuthFailureException.cs | 11 + .../WebAPI/SignalR/TokenProvider.cs | 197 ++ .../SignalR/Utils/ForeverRetryPolicy.cs | 39 + .../WebAPI/SignalR/Utils/ServerState.cs | 16 + MareSynchronos/images/icon.png | Bin 0 -> 1469866 bytes MareSynchronos/packages.lock.json | 530 +++++ UmbraAPI/.gitignore | 351 +++ UmbraAPI/LICENSE | 21 + .../MareSynchronosAPI/Data/CharacterData.cs | 36 + .../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 + UmbraAPI/MareSynchronosAPI/Data/GroupData.cs | 10 + .../Data/SignedChatMessage.cs | 14 + UmbraAPI/MareSynchronosAPI/Data/UserData.cs | 10 + .../Dto/Account/RegisterReplyDto.cs | 12 + .../Dto/Account/RegisterReplyV2Dto.cs | 11 + .../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 + .../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 + UmbraAPI/MareSynchronosAPI/Routes/MareAuth.cs | 14 + .../MareSynchronosAPI/Routes/MareFiles.cs | 45 + .../MareSynchronosAPI/SignalR/IMareHub.cs | 144 ++ .../SignalR/IMareHubClient.cs | 62 + 272 files changed, 36675 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore 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/NoSnapService.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/RemoteConfigurationService.cs create mode 100644 MareSynchronos/Services/RepoChangeConfig.cs create mode 100644 MareSynchronos/Services/RepoChangeService.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/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/UmbraSync.json 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/images/icon.png create mode 100644 MareSynchronos/packages.lock.json create mode 100644 UmbraAPI/.gitignore create mode 100644 UmbraAPI/LICENSE create mode 100644 UmbraAPI/MareSynchronosAPI/Data/CharacterData.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/ChatMessage.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Comparer/UserDataComparer.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Enum/GroupPermissions.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Enum/GroupUserInfo.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Enum/MessageSeverity.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Enum/ObjectKind.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Enum/UserPermissions.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Extensions/GroupPermissionsExtensions.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Extensions/GroupUserPermissionsExtensions.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/Extensions/UserPermissionsExtensions.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/FileReplacementData.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/GroupData.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/SignedChatMessage.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Data/UserData.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/ConnectionDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Files/DownloadFileDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Files/FilesSendDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Files/ITransferFileDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Files/UploadFileDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Group/GroupDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Group/GroupInfoDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPairDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPairUserPermissionDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/SystemInfoDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/User/UserDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/User/UserPairDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/User/UserPermissionsDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/User/UserProfileDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Dto/User/UserProfileReportDto.cs create mode 100644 UmbraAPI/MareSynchronosAPI/MareSynchronos.API.csproj create mode 100644 UmbraAPI/MareSynchronosAPI/MareSynchronosAPI.sln create mode 100644 UmbraAPI/MareSynchronosAPI/Routes/MareAuth.cs create mode 100644 UmbraAPI/MareSynchronosAPI/Routes/MareFiles.cs create mode 100644 UmbraAPI/MareSynchronosAPI/SignalR/IMareHub.cs create mode 100644 UmbraAPI/MareSynchronosAPI/SignalR/IMareHubClient.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4dedbb3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,273 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = 0 + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async +csharp_style_prefer_readonly_struct = true:suggestion + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = 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 + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +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_around_declaration_statements = false +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 + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### 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.private_or_internal_field_should_be_fieldstyle.severity = suggestion +dotnet_naming_rule.private_or_internal_field_should_be_fieldstyle.symbols = private_or_internal_field +dotnet_naming_rule.private_or_internal_field_should_be_fieldstyle.style = fieldstyle + +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.private_or_internal_field.applicable_kinds = field +dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected +dotnet_naming_symbols.private_or_internal_field.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.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.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.fieldstyle.required_prefix = _ +dotnet_naming_style.fieldstyle.required_suffix = +dotnet_naming_style.fieldstyle.word_separator = +dotnet_naming_style.fieldstyle.capitalization = camel_case +dotnet_diagnostic.MA0016.severity = silent +dotnet_diagnostic.MA0026.severity = warning +dotnet_diagnostic.MA0046.severity = suggestion +dotnet_diagnostic.MA0051.severity = suggestion +dotnet_diagnostic.MA0011.severity = suggestion + +[*.{cs,vb}] +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 +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9dea322 --- /dev/null +++ b/.gitignore @@ -0,0 +1,352 @@ +## 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 +*.bak +.DS_Store + +# 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/MareSynchronos.sln b/MareSynchronos.sln new file mode 100644 index 0000000..b442d3d --- /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", "UmbraAPI\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..b905f7c --- /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? marePath) + { + MareWatcher?.Dispose(); + if (string.IsNullOrEmpty(marePath) || !Directory.Exists(marePath)) + { + MareWatcher = null; + Logger.LogWarning("Mare file path is not set, cannot start the FSW for Mare."); + return; + } + + DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName); + StorageisNTFS = string.Equals("NTFS", di.DriveFormat, StringComparison.OrdinalIgnoreCase); + Logger.LogInformation("Mare Storage is on NTFS drive: {isNtfs}", StorageisNTFS); + + Logger.LogDebug("Initializing Mare FSW on {path}", marePath); + MareWatcher = new() + { + Path = marePath, + 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("Mare file path is not set, cannot start the FSW for Mare."); + 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("Mare 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("UmbraSync 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..8ce7fb0 --- /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 UmbraSync. After, reload UmbraSync 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..4b02452 --- /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 UmbraSync. 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..9746431 --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs @@ -0,0 +1,360 @@ +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, 0, 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 UmbraSync. 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(() => + { + var collName = "UmbraSync_" + uid; + var collId = _penumbraCreateNamedTemporaryCollection.Invoke(collName); + logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId); + return collId; + + }).ConfigureAwait(false); + } + + public async Task>?> GetCharacterData(ILogger logger, GameObjectHandler handler) + { + if (!APIAvailable) return null; + + return await _dalamudUtil.RunOnFrameworkThread(() => + { + logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths"); + var idx = handler.GetGameObject()?.ObjectIndex; + if (idx == null) return null; + return _penumbraResourcePaths.Invoke(idx.Value)[0]; + }).ConfigureAwait(false); + } + + public string GetMetaManipulations() + { + if (!APIAvailable) return string.Empty; + return _penumbraGetMetaManipulations.Invoke(); + } + + public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) + { + if (!APIAvailable || _dalamudUtil.IsZoning) return; + try + { + await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false); + await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) => + { + logger.LogDebug("[{appid}] Calling on IPC: PenumbraRedraw", applicationId); + _penumbraRedraw!.Invoke(chara.ObjectIndex, setting: RedrawType.Redraw); + + }, token).ConfigureAwait(false); + } + finally + { + _redrawManager.RedrawSemaphore.Release(); + } + } + + public 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..2c7dff4 --- /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("UmbraSyncSync.LoadMcdf"); + _loadFileProvider.RegisterFunc(LoadMcdf); + _loadFileAsyncProvider = _pi.GetIpcProvider>("UmbraSyncSync.LoadMcdfAsync"); + _loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync); + _handledGameAddresses = _pi.GetIpcProvider>("UmbraSyncSync.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..8c03281 --- /dev/null +++ b/MareSynchronos/MareConfiguration/Configurations/MareConfig.cs @@ -0,0 +1,76 @@ +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.UI; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.MareConfiguration.Configurations; + +[Serializable] +public class MareConfig : IMareConfiguration +{ + 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; } = 20; + public bool OpenGposeImportOnGposeStart { get; set; } = false; + public bool OpenPopupOnAdd { get; set; } = true; + public int ParallelDownloads { get; set; } = 10; + public int DownloadSpeedLimitInBytes { get; set; } = 0; + public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps; + [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..c8a14f5 --- /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; } = false; + public bool NotifyAutoPauseDirectPairs { get; set; } = true; + public bool NotifyAutoPauseGroupPairs { get; set; } = false; + public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 550; + public int TrisAutoPauseThresholdThousands { get; set; } = 375; + 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..77e53bd --- /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.UmbraSyncServer, ServerUri = ApiController.UmbraSyncServiceUri } }, + }; + + 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..911b263 --- /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}", "UmbraSync", version.Major, version.Minor, version.Build, version.Revision); + Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(MarePlugin), Services.Events.EventSeverity.Informational, + $"Starting UmbraSync {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 \"UmbraSync 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..4a85263 --- /dev/null +++ b/MareSynchronos/MareSynchronos.csproj @@ -0,0 +1,65 @@ + + + + UmbraSync + 1.0.0 + + + + + + + + + + + + + + + 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..145889a --- /dev/null +++ b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs @@ -0,0 +1,62 @@ +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; + private readonly NoSnapService _noSnapService; + + 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, NoSnapService noSnapService) + { + _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; + _noSnapService = noSnapService; + } + + 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, _noSnapService); + } +} \ 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..d12a347 --- /dev/null +++ b/MareSynchronos/PlayerData/Handlers/PairHandler.cs @@ -0,0 +1,916 @@ +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 readonly NoSnapService _noSnapService; + 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, + NoSnapService noSnapService) : 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; + _noSnapService = noSnapService; + + _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 void RegisterGposeClones() + { + var name = PlayerName; + if (name == null) + return; + _ = _dalamudUtil.RunOnFrameworkThread(() => + { + foreach (var actor in _dalamudUtil.GetGposeCharactersFromObjectTable()) + { + if (actor == null) continue; + var gposeName = actor.Name.TextValue; + if (!name.Equals(gposeName, StringComparison.Ordinal)) + continue; + _noSnapService.AddGposer(actor.ObjectIndex); + } + }); + } + + 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; + RegisterGposeClones(); + } + + 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; + } + } + } + } + else if (_dalamudUtil.IsInCutscene && !string.IsNullOrEmpty(name)) + { + _noSnapService.AddGposerNamed(name); + } + } + 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..d2f108c --- /dev/null +++ b/MareSynchronos/PlayerData/Pairs/Pair.cs @@ -0,0 +1,376 @@ +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 = 559, + PrefixChar = 'L' + }); + } + + 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); + + if (NoSnapService.AnyLoaded) + HoldApplication("NoSnap", maxValue: 1); + else + UnholdApplication("NoSnap", skipApplication: true); + + 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..7691d7d --- /dev/null +++ b/MareSynchronos/Plugin.cs @@ -0,0 +1,235 @@ +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 UmbraSyncSync; + +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 UmbraSyncSync 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(); + 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(); + + 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()); + 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..21e390f --- /dev/null +++ b/MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs @@ -0,0 +1,156 @@ +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 NoSnapService _noSnapService; + private readonly Dictionary _handledCharaData = new(StringComparer.Ordinal); + + public IReadOnlyDictionary HandledCharaData => _handledCharaData; + + public CharaDataCharacterHandler(ILogger logger, MareMediator mediator, + GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService, + IpcManager ipcManager, NoSnapService noSnapService) + : base(logger, mediator) + { + _gameObjectHandlerFactory = gameObjectHandlerFactory; + _dalamudUtilService = dalamudUtilService; + _ipcManager = ipcManager; + _noSnapService = noSnapService; + 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 () => + { + RemoveGposer(handled); + await RevertChara(handled.Name, handled.CustomizePlus).ConfigureAwait(false); + }).ConfigureAwait(false); + return true; + } + + internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry) + { + _handledCharaData.Add(handledCharaDataEntry.Name, handledCharaDataEntry); + _ = _dalamudUtilService.RunOnFrameworkThread(() => AddGposer(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; + } + + private int GetGposerObjectIndex(string name) + { + return _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, _dalamudUtilService.IsInGpose)?.ObjectIndex ?? -1; + } + + private void AddGposer(HandledCharaDataEntry handled) + { + int objectIndex = GetGposerObjectIndex(handled.Name); + if (objectIndex > 0) + _noSnapService.AddGposer(objectIndex); + } + + private void RemoveGposer(HandledCharaDataEntry handled) + { + int objectIndex = GetGposerObjectIndex(handled.Name); + if (objectIndex > 0) + _noSnapService.RemoveGposer(objectIndex); + } +} 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..e32b81e --- /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("[BnnuyChat] "); + _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($"[UmbraSyncSync] 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($"[UmbraSyncSync] 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..15338ec --- /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 = "/loporrit"; + + 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 UmbraSync UI" + }); + _commandManager.AddHandler(_commandName2, new CommandInfo(OnCommand) + { + HelpMessage = "Opens the UmbraSync 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("UmbraSync disconnecting", "Cannot use /toggle while UmbraSync 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..86a9621 --- /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 + UmbraSyncSync.Plugin.Self.RealOnFrameworkUpdate = this.FrameworkOnUpdate; +#pragma warning restore S2696 + _framework.Update += UmbraSyncSync.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 -= UmbraSyncSync.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/NoSnapService.cs b/MareSynchronos/Services/NoSnapService.cs new file mode 100644 index 0000000..226fab3 --- /dev/null +++ b/MareSynchronos/Services/NoSnapService.cs @@ -0,0 +1,226 @@ +using Dalamud.Plugin; +using MareSynchronos.Interop.Ipc; +using MareSynchronos.MareConfiguration.Models; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Text.Json.Serialization; + +namespace MareSynchronos.Services; + +public sealed class NoSnapService : IHostedService, IMediatorSubscriber +{ + private record NoSnapConfig + { + [JsonPropertyName("listOfPlugins")] + public string[]? ListOfPlugins { get; set; } + } + + private readonly ILogger _logger; + private readonly IDalamudPluginInterface _pluginInterface; + private readonly Dictionary _listOfPlugins = new(StringComparer.Ordinal) + { + ["Snapper"] = false, + ["Snappy"] = false, + ["Meddle.Plugin"] = false, + }; + private static readonly HashSet _gposers = new(); + private static readonly HashSet _gposersNamed = new(StringComparer.Ordinal); + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly DalamudUtilService _dalamudUtilService; + private readonly IpcManager _ipcManager; + private readonly RemoteConfigurationService _remoteConfig; + + public static bool AnyLoaded { get; private set; } = false; + public static string ActivePlugins { get; private set; } = string.Empty; + + public MareMediator Mediator { get; init; } + + public NoSnapService(ILogger logger, IDalamudPluginInterface pluginInterface, MareMediator mediator, + IHostApplicationLifetime hostApplicationLifetime, DalamudUtilService dalamudUtilService, IpcManager ipcManager, + RemoteConfigurationService remoteConfig) + { + _logger = logger; + _pluginInterface = pluginInterface; + Mediator = mediator; + _hostApplicationLifetime = hostApplicationLifetime; + _dalamudUtilService = dalamudUtilService; + _ipcManager = ipcManager; + _remoteConfig = remoteConfig; + + Mediator.Subscribe(this, msg => ClearGposeList()); + Mediator.Subscribe(this, msg => ClearGposeList()); + } + + public void AddGposer(int objectIndex) + { + if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested) + { + _logger.LogTrace("Immediately reverting object index {id}", objectIndex); + RevertAndRedraw(objectIndex); + return; + } + + _logger.LogTrace("Registering gposer object index {id}", objectIndex); + lock (_gposers) + _gposers.Add(objectIndex); + } + + public void RemoveGposer(int objectIndex) + { + _logger.LogTrace("Un-registering gposer object index {id}", objectIndex); + lock (_gposers) + _gposers.Remove(objectIndex); + } + + public void AddGposerNamed(string name) + { + if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested) + { + _logger.LogTrace("Immediately reverting {name}", name); + RevertAndRedraw(name); + return; + } + + _logger.LogTrace("Registering gposer {name}", name); + lock (_gposers) + _gposersNamed.Add(name); + } + + private void ClearGposeList() + { + if (_gposers.Count > 0 || _gposersNamed.Count > 0) + _logger.LogTrace("Clearing gposer list"); + lock (_gposers) + _gposers.Clear(); + lock (_gposersNamed) + _gposersNamed.Clear(); + } + + private void RevertAndRedraw(int objIndex, Guid applicationId = default) + { + if (applicationId == default) + applicationId = Guid.NewGuid(); + + try + { + _ipcManager.Glamourer.RevertNow(_logger, applicationId, objIndex); + _ipcManager.Penumbra.RedrawNow(_logger, applicationId, objIndex); + } + catch { } + } + + private void RevertAndRedraw(string name, Guid applicationId = default) + { + if (applicationId == default) + applicationId = Guid.NewGuid(); + + try + { + _ipcManager.Glamourer.RevertByNameNow(_logger, applicationId, name); + var addr = _dalamudUtilService.GetPlayerCharacterFromCachedTableByName(name); + if (addr != 0) + { + var obj = _dalamudUtilService.CreateGameObject(addr); + if (obj != null) + _ipcManager.Penumbra.RedrawNow(_logger, applicationId, obj.ObjectIndex); + } + } + catch { } + } + + private void RevertGposers() + { + List? gposersList = null; + List? gposersList2 = null; + + lock (_gposers) + { + if (_gposers.Count > 0) + { + gposersList = _gposers.ToList(); + _gposers.Clear(); + } + } + + lock (_gposersNamed) + { + if (_gposersNamed.Count > 0) + { + gposersList2 = _gposersNamed.ToList(); + _gposersNamed.Clear(); + } + } + + if (gposersList == null && gposersList2 == null) + return; + + _logger.LogInformation("Reverting gposers"); + + _dalamudUtilService.RunOnFrameworkThread(() => + { + Guid applicationId = Guid.NewGuid(); + + foreach (var gposer in gposersList ?? []) + RevertAndRedraw(gposer, applicationId); + + foreach (var gposerName in gposersList2 ?? []) + RevertAndRedraw(gposerName, applicationId); + }).GetAwaiter().GetResult(); + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var config = await _remoteConfig.GetConfigAsync("noSnap").ConfigureAwait(false) ?? new(); + + if (config.ListOfPlugins != null) + { + _listOfPlugins.Clear(); + foreach (var pluginName in config.ListOfPlugins) + _listOfPlugins.TryAdd(pluginName, value: false); + } + + foreach (var pluginName in _listOfPlugins.Keys) + { + _listOfPlugins[pluginName] = PluginWatcherService.GetInitialPluginState(_pluginInterface, pluginName)?.IsLoaded ?? false; + Mediator.SubscribeKeyed(this, pluginName, (msg) => + { + _listOfPlugins[pluginName] = msg.IsLoaded; + _logger.LogDebug("{pluginName} isLoaded = {isLoaded}", pluginName, msg.IsLoaded); + Update(); + }); + } + + Update(); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + RevertGposers(); + return Task.CompletedTask; + } + + private void Update() + { + bool anyLoadedNow = _listOfPlugins.Values.Any(p => p); + + if (AnyLoaded != anyLoadedNow) + { + AnyLoaded = anyLoadedNow; + Mediator.Publish(new RecalculatePerformanceMessage(null)); + + if (AnyLoaded) + { + RevertGposers(); + var pluginList = string.Join(", ", _listOfPlugins.Where(p => p.Value).Select(p => p.Key)); + Mediator.Publish(new NotificationMessage("Incompatible plugin loaded", $"Synced player appearances will not apply until incompatible plugins are disabled: {pluginList}.", + NotificationType.Error)); + ActivePlugins = pluginList; + } + else + { + ActivePlugins = string.Empty; + } + } + } +} \ No newline at end of file diff --git a/MareSynchronos/Services/NotificationService.cs b/MareSynchronos/Services/NotificationService.cs new file mode 100644 index 0000000..f3b5527 --- /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("[UmbraSyncSync] Error: " + message); + _chatGui.PrintError(se.BuiltString); + } + + private void PrintInfoChat(string? message) + { + SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSyncSync] Info: ").AddItalics(message ?? string.Empty); + _chatGui.Print(se.BuiltString); + } + + private void PrintWarnChat(string? message) + { + SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSyncSync] ").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/RemoteConfigurationService.cs b/MareSynchronos/Services/RemoteConfigurationService.cs new file mode 100644 index 0000000..763f7a1 --- /dev/null +++ b/MareSynchronos/Services/RemoteConfigurationService.cs @@ -0,0 +1,201 @@ +using Chaos.NaCl; +using MareSynchronos.MareConfiguration; +using Microsoft.Extensions.Logging; +using System.Net; +using System.Net.Http.Headers; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace MareSynchronos.Services; + +public sealed class RemoteConfigurationService +{ + private readonly static Dictionary ConfigPublicKeys = new(StringComparer.Ordinal) + { + { "UMBR4KEY", "+MwCXedODmU+yD7vtdI+Ho2iLx+PV3U0H2XRLP/gReA=" } + }; + + private readonly static string[] ConfigSources = [ + "https://umbra-sync.net/config/umbra.json" + ]; + + private readonly ILogger _logger; + private readonly RemoteConfigCacheService _configService; + private readonly Task _initTask; + + public RemoteConfigurationService(ILogger logger, RemoteConfigCacheService configService) + { + _logger = logger; + _configService = configService; + _initTask = Task.Run(DownloadConfig); + } + + public async Task GetConfigAsync(string sectionName) + { + await _initTask.ConfigureAwait(false); + if (!_configService.Current.Configuration.TryGetPropertyValue(sectionName, out var section)) + section = null; + return (section as JsonObject) ?? new(); + } + + public async Task GetConfigAsync(string sectionName) + { + try + { + var json = await GetConfigAsync(sectionName).ConfigureAwait(false); + return JsonSerializer.Deserialize(json); + } + catch (JsonException ex) + { + _logger.LogWarning(ex, "Invalid JSON in remote config: {sectionName}", sectionName); + return default; + } + } + + private async Task DownloadConfig() + { + string? jsonResponse = null; + + foreach (var remoteUrl in ConfigSources) + { + try + { + _logger.LogDebug("Fetching {url}", remoteUrl); + + using var httpClient = new HttpClient( + new HttpClientHandler + { + AllowAutoRedirect = true, + MaxAutomaticRedirections = 5 + } + ); + + httpClient.Timeout = TimeSpan.FromSeconds(6); + + var ver = Assembly.GetExecutingAssembly().GetName().Version; + httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); + + var request = new HttpRequestMessage(HttpMethod.Get, remoteUrl); + + if (remoteUrl.Equals(_configService.Current.Origin, StringComparison.Ordinal)) + { + if (!string.IsNullOrEmpty(_configService.Current.ETag)) + request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(_configService.Current.ETag)); + + if (_configService.Current.LastModified != null) + request.Headers.IfModifiedSince = _configService.Current.LastModified; + } + + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + + if (response.StatusCode == HttpStatusCode.NotModified) + { + _logger.LogDebug("Using cached remote configuration from {url}", remoteUrl); + return; + } + + response.EnsureSuccessStatusCode(); + + var contentType = response.Content.Headers.ContentType?.MediaType; + + if (contentType == null || !contentType.Equals("application/json", StringComparison.Ordinal)) + { + _logger.LogWarning("HTTP request for remote config failed: wrong MIME type"); + continue; + } + + _logger.LogInformation("Downloaded new configuration from {url}", remoteUrl); + + _configService.Current.Origin = remoteUrl; + _configService.Current.ETag = response.Headers.ETag?.ToString() ?? string.Empty; + + try + { + if (response.Content.Headers.Contains("Last-Modified")) + { + var lastModified = response.Content.Headers.GetValues("Last-Modified").First(); + _configService.Current.LastModified = DateTimeOffset.Parse(lastModified, System.Globalization.CultureInfo.InvariantCulture); + } + } + catch + { + _configService.Current.LastModified = null; + } + + jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + break; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "HTTP request for remote config failed"); + + if (remoteUrl.Equals(_configService.Current.Origin, StringComparison.Ordinal)) + { + _configService.Current.ETag = string.Empty; + _configService.Current.LastModified = null; + _configService.Save(); + } + } + } + + if (jsonResponse == null) + { + _logger.LogWarning("Could not download remote config"); + return; + } + + try + { + var jsonDoc = JsonNode.Parse(jsonResponse) as JsonObject; + + if (jsonDoc == null) + { + _logger.LogWarning("Downloaded remote config is not a JSON object"); + return; + } + + LoadConfig(jsonDoc); + } + catch (JsonException ex) + { + _logger.LogWarning(ex, "Invalid JSON in remote config response"); + } + } + + private static bool VerifySignature(string message, ulong ts, string signature, string pubKey) + { + byte[] msg = [.. BitConverter.GetBytes(ts), .. Encoding.UTF8.GetBytes(message)]; + byte[] sig = Convert.FromBase64String(signature); + byte[] pub = Convert.FromBase64String(pubKey); + return Ed25519.Verify(sig, msg, pub); + } + + private void LoadConfig(JsonObject jsonDoc) + { + var ts = jsonDoc["ts"]!.GetValue(); + + if (ts <= _configService.Current.Timestamp) + { + _logger.LogDebug("Remote configuration is not newer than cached config"); + return; + } + + var signatures = jsonDoc["sig"]!.AsObject(); + var configString = jsonDoc["config"]!.GetValue(); + bool verified = signatures.Any(sig => + ConfigPublicKeys.TryGetValue(sig.Key, out var pubKey) && + VerifySignature(configString, ts, sig.Value!.GetValue(), pubKey)); + + if (!verified) + { + _logger.LogWarning("Could not verify signature for downloaded remote config"); + return; + } + + _configService.Current.Configuration = JsonNode.Parse(configString)!.AsObject(); + _configService.Current.Timestamp = ts; + _configService.Save(); + } +} diff --git a/MareSynchronos/Services/RepoChangeConfig.cs b/MareSynchronos/Services/RepoChangeConfig.cs new file mode 100644 index 0000000..eaf0b2e --- /dev/null +++ b/MareSynchronos/Services/RepoChangeConfig.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace MareSynchronos.Services; + +public record RepoChangeConfig +{ + [JsonPropertyName("current_repo")] + public string? CurrentRepo { get; set; } + + [JsonPropertyName("valid_repos")] + public string[]? ValidRepos { get; set; } +} \ No newline at end of file diff --git a/MareSynchronos/Services/RepoChangeService.cs b/MareSynchronos/Services/RepoChangeService.cs new file mode 100644 index 0000000..3265e02 --- /dev/null +++ b/MareSynchronos/Services/RepoChangeService.cs @@ -0,0 +1,401 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using Dalamud.Utility; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Reflection; + +namespace MareSynchronos.Services; + +/* Reflection code based almost entirely on 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 sealed class RepoChangeService : IHostedService +{ + #region Reflection Helpers + private const BindingFlags AllFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; + private const BindingFlags StaticFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; + private const BindingFlags InstanceFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + private static object GetFoP(object obj, string name) + { + Type? type = obj.GetType(); + while (type != null) + { + var fieldInfo = type.GetField(name, AllFlags); + if (fieldInfo != null) + { + return fieldInfo.GetValue(obj)!; + } + var propertyInfo = type.GetProperty(name, AllFlags); + if (propertyInfo != null) + { + return propertyInfo.GetValue(obj)!; + } + type = type.BaseType; + } + throw new Exception($"Reflection GetFoP failed (not found: {obj.GetType().Name}.{name})"); + } + + private static T GetFoP(object obj, string name) + { + return (T)GetFoP(obj, name); + } + + private static void SetFoP(object obj, string name, object value) + { + var type = obj.GetType(); + var field = type.GetField(name, AllFlags); + if (field != null) + { + field.SetValue(obj, value); + } + else + { + var prop = type.GetProperty(name, AllFlags)!; + if (prop == null) + throw new Exception($"Reflection SetFoP failed (not found: {type.Name}.{name})"); + prop.SetValue(obj, value); + } + } + + private static object? Call(object obj, string name, object[] @params, bool matchExactArgumentTypes = false) + { + MethodInfo? info; + var type = obj.GetType(); + if (!matchExactArgumentTypes) + { + info = type.GetMethod(name, AllFlags); + } + else + { + info = type.GetMethod(name, AllFlags, @params.Select(x => x.GetType()).ToArray()); + } + if (info == null) + throw new Exception($"Reflection Call failed (not found: {type.Name}.{name})"); + return info.Invoke(obj, @params); + } + + private static T Call(object obj, string name, object[] @params, bool matchExactArgumentTypes = false) + { + return (T)Call(obj, name, @params, matchExactArgumentTypes)!; + } + #endregion + + #region Dalamud Reflection + public object GetService(string serviceFullName) + { + return _pluginInterface.GetType().Assembly. + GetType("Dalamud.Service`1", true)!.MakeGenericType(_pluginInterface.GetType().Assembly.GetType(serviceFullName, true)!). + GetMethod("Get")!.Invoke(null, BindingFlags.Default, null, Array.Empty(), null)!; + } + + private object GetPluginManager() + { + return _pluginInterface.GetType().Assembly. + GetType("Dalamud.Service`1", true)!.MakeGenericType(_pluginInterface.GetType().Assembly.GetType("Dalamud.Plugin.Internal.PluginManager", true)!). + GetMethod("Get")!.Invoke(null, BindingFlags.Default, null, Array.Empty(), null)!; + } + + private void ReloadPluginMasters() + { + var mgr = GetService("Dalamud.Plugin.Internal.PluginManager"); + var pluginReload = mgr.GetType().GetMethod("SetPluginReposFromConfigAsync", BindingFlags.Instance | BindingFlags.Public)!; + pluginReload.Invoke(mgr, [true]); + } + + public void SaveDalamudConfig() + { + var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration"); + var configSave = conf?.GetType().GetMethod("QueueSave", BindingFlags.Instance | BindingFlags.Public); + configSave?.Invoke(conf, null); + } + + private IEnumerable GetRepoByURL(string repoURL) + { + var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration"); + var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList"); + foreach (var r in repolist) + { + if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase)) + yield return r; + } + } + + private bool HasRepo(string repoURL) + { + var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration"); + var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList"); + foreach (var r in repolist) + { + if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + + private void AddRepo(string repoURL, bool enabled) + { + var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration"); + var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList"); + foreach (var r in repolist) + { + if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase)) + return; + } + var instance = Activator.CreateInstance(_pluginInterface.GetType().Assembly.GetType("Dalamud.Configuration.ThirdPartyRepoSettings")!)!; + SetFoP(instance, "Url", repoURL); + SetFoP(instance, "IsEnabled", enabled); + GetFoP(conf, "ThirdRepoList").Add(instance!); + } + + private void RemoveRepo(string repoURL) + { + var toRemove = new List(); + var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration"); + var repolist = (System.Collections.IList)GetFoP(conf, "ThirdRepoList"); + foreach (var r in repolist) + { + if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase)) + toRemove.Add(r); + } + foreach (var r in toRemove) + repolist.Remove(r); + } + + public List<(object LocalPlugin, string InstalledFromUrl)> GetLocalPluginsByName(string internalName) + { + List<(object LocalPlugin, string RepoURL)> result = []; + + var pluginManager = GetPluginManager(); + var installedPlugins = (System.Collections.IList)pluginManager.GetType().GetProperty("InstalledPlugins")!.GetValue(pluginManager)!; + + foreach (var plugin in installedPlugins) + { + if (((string)plugin.GetType().GetProperty("InternalName")!.GetValue(plugin)!).Equals(internalName, StringComparison.Ordinal)) + { + var type = plugin.GetType(); + if (type.Name.Equals("LocalDevPlugin", StringComparison.Ordinal)) + continue; + var manifest = GetFoP(plugin, "manifest"); + string installedFromUrl = (string)GetFoP(manifest, "InstalledFromUrl"); + result.Add((plugin, installedFromUrl)); + } + } + + return result; + } + #endregion + + private readonly ILogger _logger; + private readonly RemoteConfigurationService _remoteConfig; + private readonly IDalamudPluginInterface _pluginInterface; + private readonly IFramework _framework; + + public RepoChangeService(ILogger logger, RemoteConfigurationService remoteConfig, IDalamudPluginInterface pluginInterface, IFramework framework) + { + _logger = logger; + _remoteConfig = remoteConfig; + _pluginInterface = pluginInterface; + _framework = framework; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("Starting RepoChange Service"); + var repoChangeConfig = await _remoteConfig.GetConfigAsync("repoChange").ConfigureAwait(false) ?? new(); + + var currentRepo = repoChangeConfig.CurrentRepo; + var validRepos = (repoChangeConfig.ValidRepos ?? []).ToList(); + + if (!currentRepo.IsNullOrEmpty() && !validRepos.Contains(currentRepo, StringComparer.Ordinal)) + validRepos.Add(currentRepo); + + if (validRepos.Count == 0) + { + _logger.LogInformation("No valid repos configured, skipping"); + return; + } + + await _framework.RunOnTick(() => + { + try + { + var internalName = Assembly.GetExecutingAssembly().GetName().Name!; + var localPlugins = GetLocalPluginsByName(internalName); + + var suffix = string.Empty; + + if (localPlugins.Count == 0) + { + _logger.LogInformation("Skipping: No intalled plugin found"); + return; + } + + var hasValidCustomRepoUrl = false; + + foreach (var vr in validRepos) + { + var vrCN = vr.Replace(".json", "_CN.json", StringComparison.Ordinal); + var vrKR = vr.Replace(".json", "_KR.json", StringComparison.Ordinal); + if (HasRepo(vr) || HasRepo(vrCN) || HasRepo(vrKR)) + { + hasValidCustomRepoUrl = true; + break; + } + } + + List oldRepos = []; + var pluginRepoUrl = localPlugins[0].InstalledFromUrl; + + if (pluginRepoUrl.Contains("_CN.json", StringComparison.Ordinal)) + suffix = "_CN"; + else if (pluginRepoUrl.Contains("_KR.json", StringComparison.Ordinal)) + suffix = "_KR"; + + bool hasOldPluginRepoUrl = false; + + foreach (var plugin in localPlugins) + { + foreach (var vr in validRepos) + { + var validRepo = vr.Replace(".json", $"{suffix}.json"); + if (!plugin.InstalledFromUrl.Equals(validRepo, StringComparison.Ordinal)) + { + oldRepos.Add(plugin.InstalledFromUrl); + hasOldPluginRepoUrl = true; + } + } + } + + if (hasValidCustomRepoUrl) + { + if (hasOldPluginRepoUrl) + _logger.LogInformation("Result: Repo URL is up to date, but plugin install source is incorrect"); + else + _logger.LogInformation("Result: Repo URL is up to date"); + } + else + { + _logger.LogInformation("Result: Repo URL needs to be replaced"); + } + + if (currentRepo.IsNullOrEmpty()) + { + _logger.LogWarning("No current repo URL configured"); + return; + } + + // Pre-test plugin repo url rewriting to ensure it succeeds before replacing the custom repo URL + if (hasOldPluginRepoUrl) + { + foreach (var plugin in localPlugins) + { + var manifest = GetFoP(plugin.LocalPlugin, "manifest"); + if (manifest == null) + throw new Exception("Plugin manifest is null"); + var manifestFile = GetFoP(plugin.LocalPlugin, "manifestFile"); + if (manifestFile == null) + throw new Exception("Plugin manifestFile is null"); + var repo = GetFoP(manifest, "InstalledFromUrl"); + if (((string)repo).IsNullOrEmpty()) + throw new Exception("Plugin repo url is null or empty"); + SetFoP(manifest, "InstalledFromUrl", repo); + } + } + + if (!hasValidCustomRepoUrl) + { + try + { + foreach (var oldRepo in oldRepos) + { + _logger.LogInformation("* Removing old repo: {r}", oldRepo); + RemoveRepo(oldRepo); + } + } + finally + { + _logger.LogInformation("* Adding current repo: {r}", currentRepo); + AddRepo(currentRepo, true); + } + } + + // This time do it for real, and crash the game if we fail, to avoid saving a broken state + if (hasOldPluginRepoUrl) + { + try + { + _logger.LogInformation("* Updating plugins"); + foreach (var plugin in localPlugins) + { + var manifest = GetFoP(plugin.LocalPlugin, "manifest"); + if (manifest == null) + throw new Exception("Plugin manifest is null"); + var manifestFile = GetFoP(plugin.LocalPlugin, "manifestFile"); + if (manifestFile == null) + throw new Exception("Plugin manifestFile is null"); + var repo = GetFoP(manifest, "InstalledFromUrl"); + if (((string)repo).IsNullOrEmpty()) + throw new Exception("Plugin repo url is null or empty"); + SetFoP(manifest, "InstalledFromUrl", currentRepo); + Call(manifest, "Save", [manifestFile, "RepoChange"]); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception while changing plugin install repo"); + foreach (var oldRepo in oldRepos) + { + _logger.LogInformation("* Restoring old repo: {r}", oldRepo); + AddRepo(oldRepo, true); + } + } + } + + if (!hasValidCustomRepoUrl || hasOldPluginRepoUrl) + { + _logger.LogInformation("* Saving dalamud config"); + SaveDalamudConfig(); + _logger.LogInformation("* Reloading plugin masters"); + ReloadPluginMasters(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception in RepoChangeService"); + } + }, default, 10, cancellationToken).ConfigureAwait(false); + _logger.LogInformation("Started RepoChangeService"); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _ = cancellationToken; + _logger.LogDebug("Stopping RepoChange Service"); + return Task.CompletedTask; + } +} diff --git a/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs b/MareSynchronos/Services/ServerConfiguration/ServerConfigurationManager.cs new file mode 100644 index 0000000..7e8056d --- /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 lopExists = false; + for (int i = 0; i < _configService.Current.ServerStorage.Count; ++i) + { + var x = _configService.Current.ServerStorage[i]; + if (x.ServerUri.Equals(ApiController.UmbraSyncServiceUri, StringComparison.OrdinalIgnoreCase)) + lopExists = true; + } + if (!lopExists) + { + _logger.LogDebug("Re-adding missing server {uri}", ApiController.UmbraSyncServiceUri); + _configService.Current.ServerStorage.Insert(0, new ServerStorage() { ServerUri = ApiController.UmbraSyncServiceUri, ServerName = ApiController.UmbraSyncServer }); + 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/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..88ddb4e --- /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, "UmbraSync Character Data Hub###UmbraSyncCharaDataUI", 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..8655573 --- /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, "###UmbraSyncSyncMainUI", 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 = $"UmbraSync Sync {dev} ({ver.Major}.{ver.Minor}.{ver.Build})###UmbraSyncSyncMainUIDev"; + Toggle(); +#else + var ver = Assembly.GetExecutingAssembly().GetName().Version!; + WindowName = "UmbraSync Sync " + ver.Major + "." + ver.Minor + "." + ver.Build + "###UmbraSyncSyncMainUI"; +#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.UmbraSyncServiceUri, StringComparison.Ordinal)) + UiSharedService.AccentColor = new Vector4(1.0f, 0.8666f, 0.06666f, 1.0f); + 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 UmbraSync installation is out of date, the current version is {ver.Major}.{ver.Minor}.{ver.Build}. " + + $"It is highly recommended to keep UmbraSync 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 UmbraSync 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..e6fc546 --- /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, "UmbraSync 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, 220, transparency), 1); + drawList.AddRectFilled(dlBarStart, dlBarEnd, + UiSharedService.Color(0, 0, 0, transparency), 1); + var dlProgressPercent = transferredBytes / (double)totalBytes; + drawList.AddRectFilled(dlBarStart, + dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) }, + UiSharedService.Color(50, 205, 50, 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..14e535e --- /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("UmbraSync"); + 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 = $"UmbraSync: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}"; + colors = _configService.Current.DtrColorsPairsInRange; + } + else + { + tooltip = "UmbraSync: Connected"; + colors = _configService.Current.DtrColorsDefault; + } + } + else + { + text = RenderDtrStyle(_configService.Current.DtrStyle, "\uE04C"); + tooltip = "UmbraSync: 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..d0c0628 --- /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, "UmbraSync Edit Profile###UmbraSyncSyncEditProfileUI", 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..509d9b0 --- /dev/null +++ b/MareSynchronos/UI/IntroUI.cs @@ -0,0 +1,369 @@ +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, "UmbraSync 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 UmbraSync"); + ImGui.Separator(); + UiSharedService.TextWrapped("UmbraSync 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(""" +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(""" +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, UmbraSync will have to scan your Penumbra mod directory. " + + "Additionally, a local storage folder must be set where UmbraSync 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 UmbraSync 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 UmbraSync. 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 UmbraSync 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 UmbraSync 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 UmbraSync before, click below to register a new account."); + if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Register a new UmbraSync 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..0b233cf --- /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 + "###UmbraSyncSyncPermissions" + 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..1806524 --- /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 + "###UmbraSyncPairAnalysis" + 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..1d051c8 --- /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, "###UmbraSyncSyncPopoutProfileUI", 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..383cdff --- /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, "UmbraSync 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 Mare Synchronos API", ref mareApi)) + { + _configService.Current.MareAPI = mareApi; + _configService.Save(); + _ipcProvider.HandleMareImpersonation(); + } + _uiShared.DrawHelpText("Enables handling of the Mare Synchronos API. This currently includes:\n\n" + + " - MCDF loading support for other plugins\n" + + " - Blocking Moodles applications to paired users\n\n" + + "If the Mare Synchronos 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("Mare API active!", ImGuiColors.HealerGreen); + } + else + { + if (!mareApi) + UiSharedService.ColorTextWrapped("Mare API inactive: Option is disabled", ImGuiColors.DalamudYellow); + else if (_ipcProvider.MarePluginEnabled) + UiSharedService.ColorTextWrapped("Mare API inactive: Mare plugin is loaded", ImGuiColors.DalamudYellow); + else + UiSharedService.ColorTextWrapped("Mare 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("UmbraSync 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 UmbraSync 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 UmbraSync 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 UmbraSync Storage. " + + "Do not stop the monitoring, unless you plan to move the Penumbra and UmbraSync Storage folders, to ensure correct functionality of UmbraSync." + 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 UmbraSync 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 + + "UmbraSync'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 UmbraSync 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 UmbraSync 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 UmbraSync 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.UmbraSyncServer, 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..88a3573 --- /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 + "##UmbraSyncSyncStandaloneProfileUI" + 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..cdbfb6f --- /dev/null +++ b/MareSynchronos/UI/UISharedService.cs @@ -0,0 +1,1025 @@ +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:\\UmbraSyncStorage) 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 UmbraSync 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 UmbraSync 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 UmbraSync. 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 UmbraSync."); + return false; + } + else if (NoSnapService.AnyLoaded) + { + IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow); + ImGui.SameLine(); + var cursorX = ImGui.GetCursorPosX(); + ImGui.TextColored(ImGuiColors.DalamudYellow, "Synced player appearances will not apply until incompatible plugins are disabled:"); + ImGui.SetCursorPosX(cursorX + 16.0f); + ImGui.TextColored(ImGuiColors.DalamudYellow, NoSnapService.ActivePlugins); + 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/UmbraSync.json b/MareSynchronos/UmbraSync.json new file mode 100644 index 0000000..b147c8d --- /dev/null +++ b/MareSynchronos/UmbraSync.json @@ -0,0 +1,14 @@ +{ + "Author": "SirConstance", + "Name": "UmbraSync", + "Punchline": "Il revient!", + "Description": "This plugin will synchronize your Penumbra mods and current Glamourer state with other paired clients automatically.", + "InternalName": "UmbraSync", + "ApplicableVersion": "any", + "Tags": [ + "customization" + ], + "IconUrl": "", + "RepoUrl": "", + "CanUnloadAsync": true +} 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..5723f5a --- /dev/null +++ b/MareSynchronos/Utils/PngHdr.cs @@ -0,0 +1,49 @@ +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 = BitConverter.ToUInt32(buffer); + + // 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 = BitConverter.ToUInt32(buffer); + uint height = BitConverter.ToUInt32(buffer[4..]); + + // 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; + } + } +} 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..fab4de0 --- /dev/null +++ b/MareSynchronos/WebAPI/AccountRegistrationService.cs @@ -0,0 +1,84 @@ +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 readonly RemoteConfigurationService _remoteConfig; + + private string GenerateSecretKey() + { + return Convert.ToHexString(SHA256.HashData(RandomNumberGenerator.GetBytes(64))); + } + + public AccountRegistrationService(ILogger logger, ServerConfigurationManager serverManager, RemoteConfigurationService remoteConfig) + { + _logger = logger; + _serverManager = serverManager; + _remoteConfig = remoteConfig; + _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 authApiUrl = _serverManager.CurrentApiUrl; + + // Override the API URL used for auth from remote config, if one is available + if (authApiUrl.Equals(ApiController.UmbraSyncServiceUri, StringComparison.Ordinal)) + { + var config = await _remoteConfig.GetConfigAsync("mainServer").ConfigureAwait(false) ?? new(); + if (!string.IsNullOrEmpty(config.ApiUrl)) + authApiUrl = config.ApiUrl; + else + authApiUrl = ApiController.UmbraSyncServiceApiUri; + } + + var secretKey = GenerateSecretKey(); + var hashedSecretKey = secretKey.GetHash256(); + + Uri postUri = MareAuth.AuthRegisterV2FullPath(new Uri(authApiUrl + .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..384cad7 --- /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 UmbraSyncServer = "UmbraSync Main Server (BETA)"; +public const string UmbraSyncServiceUri = "wss://umbra-sync.net/"; +public const string UmbraSyncServiceApiUri = "wss://umbra-sync.net/"; +public const string UmbraSyncServiceHubUri = "wss://umbra-sync.net/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 UmbraSync 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 UmbraSync 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..2013231 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/HubFactory.cs @@ -0,0 +1,240 @@ +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 RemoteConfigurationService _remoteConfig; + 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, RemoteConfigurationService remoteConfig, + TokenProvider tokenProvider, ILoggerProvider pluginLog) : base(logger, mediator) + { + _serverConfigurationManager = serverConfigurationManager; + _remoteConfig = remoteConfig; + _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 = [] + }; + } + + if (_serverConfigurationManager.CurrentApiUrl.Equals(ApiController.UmbraSyncServiceUri, StringComparison.Ordinal)) + { + var mainServerConfig = await _remoteConfig.GetConfigAsync("mainServer").ConfigureAwait(false) ?? new(); + defaultConfig = mainServerConfig; + if (string.IsNullOrEmpty(mainServerConfig.ApiUrl)) + defaultConfig.ApiUrl = ApiController.UmbraSyncServiceApiUri; + if (string.IsNullOrEmpty(mainServerConfig.HubUrl)) + defaultConfig.HubUrl = ApiController.UmbraSyncServiceHubUri; + } + + 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/umbra/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..f7e57d7 --- /dev/null +++ b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs @@ -0,0 +1,197 @@ +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 RemoteConfigurationService _remoteConfig; + private readonly ConcurrentDictionary _tokenCache = new(); + private readonly ConcurrentDictionary _wellKnownCache = new(StringComparer.Ordinal); + + public TokenProvider(ILogger logger, ServerConfigurationManager serverManager, RemoteConfigurationService remoteConfig, + DalamudUtilService dalamudUtil, MareMediator mareMediator) + { + _logger = logger; + _serverManager = serverManager; + _remoteConfig = remoteConfig; + _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; + + var authApiUrl = _serverManager.CurrentApiUrl; + + // Override the API URL used for auth from remote config, if one is available + if (authApiUrl.Equals(ApiController.UmbraSyncServiceUri, StringComparison.Ordinal)) + { + var config = await _remoteConfig.GetConfigAsync("mainServer").ConfigureAwait(false) ?? new(); + if (!string.IsNullOrEmpty(config.ApiUrl)) + authApiUrl = config.ApiUrl; + else + authApiUrl = ApiController.UmbraSyncServiceApiUri; + } + + try + { + _logger.LogDebug("GetNewToken: Requesting"); + + tokenUri = MareAuth.AuthV2FullPath(new Uri(authApiUrl + .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/images/icon.png b/MareSynchronos/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c613251e5d91d29e371d46b69f0e27f8188aff22 GIT binary patch literal 1469866 zcmZ^}1CS@*vMu~;yL;NUZDZQDZQJIwZQHh{ZQHgzZFj%;qy0h@R|2u)23CIWl0QIpj9|mB5 z*91o5iZTFzCm8_X9|Qor{dM`D0st-y0KmCE0Kk<70HE7vwJY%a4Fs8}OPI>Y0I2@j zkN_|sBml@?3+V3&0Kx+NlkKkskOad1zqTR}#eZS`mJn+3uX_H$X#O4l9O8e+KkI+b zAbCLl$(RTH-)K-k9>{;&AjALAv|1~+{!UQ#;u=l>01Wa!0|=0jg#iEnPgp3cJFCk` za~av$&>0xp8Jf_!+t~lZ0`RzV{WWb&oDB%vZLDpbxZHV({)OQBYyab>CnES4#Mz3M zNL@yrK*-L~gn*5XfsTQQ51N30fXC6;luJ=q^uOqTSG+{#&d&B+^z?3SZgg(Ubaswr z^o*RGob(J#^h`{&e-X4!9=6T~?zFZ}#Q#q6-+6>hoQxbT?42#_Yzh9!YhY;S;>=4# z^pDX0J^o#%vxVt@lx&^;tF6Bc(*F~oXQX4G|9{a;+%5ipX#Yh1P5W10|CZzVCmEN9 zqKT88waY(c@v$@U{7d2gwo(G2mYV#xa2L|O{~?0Eo@9|o&M>Ck%5VYhyMQ=`rlL`J8L^fC3^!S z6TZJn|Dya4*Z)TUOGx8CLY!=D|5N9GME;A(L;uh1{vWsh-+Sv{-oKlR51NPm|87)1 zX!FLHUI2g}AR#QE><%QorQ?Ysm5^nyfAVlTS8S@OHLK!gC9wWY1G%Hiw$}IlTzm7}s@K!)=exbV zgJs$BvufLQyzjR5SS*Vm`#vyM$5I|bm`XoiNzSi1dpM(Li9KO8mg}_^EciS@Sqaa!xRyck*A5K z_Z=Ijz-Tv3#0Wa7M~>5K;R=C+UNNeXgmIdF$sZBgQ_YjYe1DLnq(xPfS1`&Qs`s^z zOzqU4JoX~tpo|4;&nvBf#sEq|lJ%|19A{+RqcV38ZKA>v1Qi&a(PR9+G#eMO>B6JV1Fcq-o(njT+0JSV>Cq! z{t1uale_?Nfs(mTdH+~L#9xC|ebHG%-K=iQkIcK-l#on?ay}X*V4mvYWMZg;oz4+~ z*r-xYI0C`4D|i}b@cGUOQy#uDshwiCb^8RxnSN-7uTnybjK-oak-OyduJchVVIgp4 zquHB+oqMLL9*$l2N^LqQWgq9}l-Flm>E8Ub`LL^Z#Usvx?;P$JD zuZ(c`(7J1-2ztvcgoMI2ku`dirZfm;o!FTsIYASBgA-NHIeiT$1IpmT>+P$vQlwYY zN}|c4x|chWIm90&2WcP8y4f6MrNIXbf)2-U4`u98@Ep{>JCnAtb)*|r;B@&R<;U+YR@$Z zy8c(5)wVpW%`@5?*1en5sX#6NEw$pVccaGJ=%ut)PFIK5QaR>E~Y(%TWnvt06mM7i%{X zMG%SkV1gdjAJ8byqoMiMt#oi-f90?B$}l1n8e%DmHwBO-CL3@OpWq^6<4oh8Qk+pu zhz?*tgA7*}%E{oVrqNyF>-A`5Ece?f1~llU;TPRII4(Q;weO?us#YyL2G|qtjHb8; zObEir)73?=^}$-;q3g&N>UKu#mdr30r`o2}jM|C`+j4X$D!>iYKE&QOf3nG&o2Yj? zPsQEQrwyB*Gbz=G$((;U30@$&`H&UGGZxjE%PqI+CLW~cSbW&*&)aY~txO17q=|Oa zwHayre5OIG?(%#UPrt@*%pWVs53~QhetzDzerTqnwhbXP;*7WJ#;zgb{rz^eO(8q< z{iXhD+%|Q0VE6qPOIv$+;h%T!a)sJ;z^fpUkLFP*o>%0x(*ZN#t5CeOvbpe}IU>El zSR`7m2RsjKS0JOIIAk-eCiLir{g&h!0oWxQA~+bS0c^?+$-%L}(A(MB93-k3&TbhC ztkEWB;w8d%u99O3Z*RGZXI@%v6?FCZ%KHuA> zZ9Hrkz6Z`YxfJOqM8F0VoJZ`jq_c%~)UXCZ0$PQ;_!|P|hmF5H5jy58_N#S_K8&n1 zAWz1w`h^SS5jb8b*13x9NmXkM!j^Kkw0K_~$|v%CSiZ(HWf7*^t*6+N#KFi{&N;;1 zMciD(QD3QW=bQ8;gS(bTmWAR=@*rhSv;LIG(_4Yi>7b8j?eUgSd@NYBAw{}nMo2=* z1*HY8YQczZt1Vp(C-;v@g)_JW<;W|F|HLsGJ+^x-x?eY@p)WvrF*upWM zg^Z`?s#@I~R^Fn^zr&22wn{<^5a}X=grf$%?p!H1HH?->yF(;xF0b206TdZ2$`*AY zo3st~LT&bFHo@to?Jp^rxF_Sfnn*%g)ZJ!ESbXGx;IaVO$aKETD?`9S!zrx5 zL2Wn9rJGeTh(=A#ov9gF)-58ap$CZ@p*^Otc{T6`E%YLASJ7a6A{|yeLTeZ$&G?am z(N48Z9124^0HMhV#se+0n@qU<2De2vk+G|wAxy{tYzXAX;e;;kADks{=d8gmS}=kX zr>_aCNFv69V`WS|xQ#r1?GV|_P#J0DH;Fg3q`qjrKjwa!kunfvG#gJ_q%?53CJQH2 z1A{#^YAe5}3rUdqDy_D+9(v!MJhv8

h1yUhA#uU=8g=kzcWW$gHpcYo1}iZ2IbZ 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

>+P6r=T_#a>b4b8$Bg+j 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

}Q&z+q5i6Pglx)dv!12Ms#>xtp?X|e~W1w&7 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%0D%eB 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⁣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�Xend-_iK_QUBH0#k)tnCqF6 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�tpV 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

?zs<1-Cp>xiR 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

#HG=;G}BrVBM3EKaRT zfaZs>Cn(L-B6J5$3|6vh6^T$7%8<>MM2Aj}b^ND`pOE=W%oo}t!T3{dU+;Z%p#w}p zPjMo-(06_P;Q(%cLCupr(RL4`7T<0js{7bvckX5HRm(3X* zmwaVSP{+lIJeB~M;j2bE4VlT(a486=TLC-;R;EaVy*9s2=VHYuENo+?XqH7Ra#%FA z=gat%g6W)y!>(vSklr#cG5k*L3M{OID=BgsFfjezZObXCM3wS}S=yqc)TSu_7q4F2|M6KL7Nu|8?)$4LRC7k5rKJD3l|neB2zNNC=b@H`C;2 zN+=+UI5naqr~dIzt~;0KVu0rz6Y30;s#DUL{sq9^zH2#Gg%w|(;UY%(hBIkg#l1N) zu3E`tbFM#5HLP+*l_Yn%_-v}r6{jHt*8V0m;6saZjbaP!!oAA#U!Ca0kv#Z|gZyP8 zaga`tC?rNd3bHPSiARfs2_6TTtGYchjiWX4Wyq^U;U4 zO?>0scOE{x|NTF{v@2XwCN@LR0<27DL2T6H%rE8-<6x2h)re-6z64$-$mPaQQrz^x zG>gwnPg>Bw`q{D5moA)p;j;JAdsV{PiIWFCb~bB=tV86CCe%Ff`*?fTZb&&#k^o_G_giRC{%@La!#QEM3~hQkh*N`Lz?rSGa*;WFoqN*aX1Ar-m|#TU=7D z{%%j&(uZ~GVCQ`DwvAT1Kc156=9dLQQCrhyYn%4_8(R{-7DE{h(PwnY$uZJ8JEAc) z$MH)`2R!L|)KeeZ4%Dv37yEV4GEtv;e{KvO&O5HNC9Ia=3{OsAY7cO1h_%Zk#3_V)IXwKdP- z_bfV6;%&MVw5FV32drnft^Bc}mK%Nd?>#VN>S95W0Hc0(_mllbrUS@jCR4FxT)b=C z{j@qpwbtfsKYC;a_s+E&H?Mqm=Zzl`y7J{$dfxi}=67k;kJvbTcz5UVcmMnhESFw; zsbt{?!yrx&+S93!P6Aj8&ypVb&1vvqpx9M{+C~w_&Yrch$n_&@CorPnjs=1H57;`% z#$2EO^{-!k)Qf)2^dCQa21H`kZ9rathaqqr{V7)ZHyM}N0(5vL(AeCOV);h{A7{xn z&Z<7?7aW|0Pj9&FhA0lv;~eQRiWH1lR=GkaLXL}w#6jO-9LHB(OD@)Rwy+>`Oxb>V zilzOm(UqZ!CyxCl&=gG2RE3(5*B$7DJmk~qnHgNv!E>SvAtk0KJ9J`|yQ+c=UVDy| z6KLC#)YbZ8$>*q>ex4x*4hdv&WY8g%1iy5}KYBRD~^XsR8tkl@zUAGiMR|Gv3# z|MmCYTUkA7$1XKysOfoueO_1j>wo)S!2H8!pFj0n`_WbN)r(;H5h+VrVG0QHu|QXS zB`*sl9nr_#iDKlP3C0G@MP3q5^D}jVH$y~gWLzAl7R80B8Av$?-$lE4$Y^RKRF@aQ zcosSebgUFHGhs+}Ofr8Gu!@O}cCN2no!81bS)7y$xNO0%TX$&*OM#1t}UP6UV>N+H$3PhNxi=}JmRIGH1QHg#`lus3`bN;kj7$yi3 zb!!Dr|Cg?G2a5I>#B@>+Ot5j-U`p~KJq(DKo&}KKT&_l)epG{SUW9p)&d(q|RT4De zkV>9%m%_6yxl^bvZkm~qZMOt_spUS5v8Qg2*`ycJOGhC$$`(r)>p~}3VKA+zvYkSc(G;GgvFwH>Jv8Ndmy+i zK>?t2ii{aKfqHa$#HwtF`6QrIefn1o!jFH36#M4O)EXVL+Nic5jO60CuukCx9&qPQ z0-U4_>V{abq0$kiI4kQB&RRj34fCYTOueIK@JpXw2A2TUnuLK&nK^3t5W6Jyq;V?F zoz77;Cb(f4@Qa?An3!fl$1ZtLJ>Es2P*GNyMWYBOa-M-dD?1E{J+LAvf?CzaJ(3k2 z7VXwhIZ(2-|MugRV@HmkyYz75{=I8Ie(-<&x1C2DZ@&MlmDN>G^|kWHJy;89j~_kw z{HR!k@zdw_`zL0ZX||6ly@f|MT$=XMmK)&ze|VJh+M;js?%jX<*PriOr>k!_C=n`Q z=yteOLpQH{>lLjACzf`2ILsn)U<7Sr%)rP|17IShV$NMbjr5wCy#qvy$v+2m|8j51nRXL?id_8W=G3e zK(nxSj{|gTM=!tjrv36CKkbf3o-=-cPYC6SnFnj(@aVzDL;FRx(dTMI_9Su*Rx@yW z{@j|YGZ$^$Q+ZWq=*PYaWjXH6D+Hq_fFEx@>Ly}MnE&-Y>g|{r?~eJ3tyB*mv0Y;u z=~OYxjCfKL@8&j=cF8Y zKwS>W(%j0IY&HK5SnGA9&Sh>n;>%j;f(cl9VO_?>zg7FCL&}t#d?U8-G@WY;+VoN$ z?Mjs{$FTs%3VGyQzEu;#1&l9iZBO$Km9NxhRN2UZWAJadK|9%VJ4 z$XE?!UDS5uD%K@G@hSk^d3Ie%NQ|$4~HDT_}Z&KeDOsaUBf7u$Q$c=c+a2M1oOeY zd&f>3H~g^DCj)~=;&J24fXOcSmiaw>;w1oAuU)%&_3E8lHe z)BA=a+2Yw-2!gyuAO&0*5!%?%BQcEqbp64DFFyRR{O|1+N8QIO`=aN!d0?6S;2|$s z{^rwQNN#hvY174mxB?kpb5Ewoy zR*;17+?S~53L1!M002M$Nklh0GRaaa$lRc?JLprb;&VOID? zV`2kgxYT+OCQIf$sRu8mXA2EOq>?_4(Lkx591cV* zEK{xY&<#Q%F;sZpRBn-SEoWq6URYJK!wJL8ju^I$bD1cTh~_mh_kWagjngdkoKdPK zbsB={w8kJQiIxULQLUNH=7RP;-@J3@(?9)bdwuhr-~aa5iBqzYaFVr{OLjee?(DDr ze|MvL*;F=k@sNrDV)ps8+CE{^MdPV2_?>J0andnl8qPtR%; z7}%$ka99>BrlX@ zr~ai$2~E3CDvx&|RVal$IB9H~h`#{=b}iEeUo zMitENj8iJVB+vv56v;l-oX+0Z`Coy-2h$>8PG{+=69DK1P+z;%xJao=1ehU}+)a>x z7NnDkYPyOOqk<#t1yRz8Sc0&&O+B5bqORvw_NBfrfOo9AZZ`;T}FA#L=Gtj zX-ke?{j0zI?emZS_Go=$^?>c*CVl0;=x6bcbP&}o}ky0FsaQDa`{iycoB6 zO{$O7=;-H5-y)^X9MzC2Ce!&Lvij;SDi$m%bWl7;iwiczsB}I-Fk5Qdrpi{F zI~Ml8^M~Jhf#=8n&woC=dF!M9vNh25oA1AO;*6Kt>fYFz=ZQ({#UELru`r~n zt1<@M>th~68RLw)egL-iyUw?q%V=xMo8s<5pR5>{3(Cw@PwjxzM-+8jW zb7%d|ogcsX>X^F#XV0EEckayjv!)VHoj&V{$a{BfJ9@WX(aeRQkdx$?=srJl>GH+b zUIB$oH5*7`91bB-er0x=iy_qy?B9I&aQ*Is=>Sr@z|S|6yd76fh@U=p)=hxN&pnf> zD?<7}P6<9YMx0eomH=(a3>dj6!zAlit9!fybzdB?#6M~|QJ%ShH05nxV9GS?4`@A$JVwt9)hFb8!y3=w;IRnY-tE;Pqe`Ddz(cTA>XaVT;c z#nK`_^)+mhgdD}Amx1XAwT@uAvJwEy1~)>E8^Gbo&eLmOe}DF+uiyUF`<@ptm^!H2 zB>B{0p%e}??Uv7kZ~k6{m0F6Vu_NXlpRinmEc*W7KVsmxa{uNni~hg=^iz8oubw#d z^MCo5_x|vQZ@>75_haZit~|5(q&bd+Mx16^mVA|<8EovfWQUpun&qlw(v>%MNQ^9- zed(p6<%)bS)2As?<^L`VqP@X4&zmM};F_Q_@r*y5!EPAQg@P$NOj=5=+;MK8MjW8Z zRM)3sEPy~M4uu(=)Upu4h@_mV>|T-FKEdkIu*4h`lKLqzMXi|grJRkCd{HJ$z~MEF z2ud)Sx`7Qv^zRE_rw$)n z-EW8G&Cmb#fk};b|L_}cfv^FDJ*i6xb}8O@cI3pF_x}BVvES5JfBSgz-mT}m&w51w z8Yqkcvc^H3)cke)WrC!KG=7Sj`t^+mYAq+vEMAsOMQ8f>La#}1Ng9k52Vznad+_8U z9HX9*UWyAn;TAL#A>l&NBH3^7hXVS9Y&fH`d6sOZpx;o6UQL<83Ysq+3Ns^}|Bn~? z(`=*ieh25`alt#HT)}bfJMJ}t!Fyb0aXO(+o-pz-lrGEU&GJfERwKz`T$*cH!{lOhyS$X~ zrSVXll^@_Rjty%%H7)>?q^%6y4bGN_>10~RGpV1sSx;WkgJ{7` z$iiB8zuMzCGNHNQbh25Xrv~}vnKKt&d;eFj|Ki=F$4<2TV?-!SOvS7mw6giL5B~D? z-##>Fe8dVrZq^W?ML>(^rQ5h88R*s%kw6r?K8aB`qofm42kE@%t56kk-Q>~P$sFj8T?o`e8-Qr2HQ^NUjkH zW%>whlfJqg3F}N{3W-LVGh`Svu_{xg6S_IwfL@KnQ`=ugOKs>^aPEsE2bBn>l}iXr zHjYf?mpXZW--dpoD64^PD2n3zS^S*(`)!z#Zpq%j-WYzy&;Ikz>ksb#{P(}NLdG<(tp+ZBGJrOd%(X%-P*BY+TqUhit~%NLxZP@32SlB(M|bZ&x^?@yjndancuM%>g$rlT zp4XRhYint=R3r|HNa|~?9AA6o9n=43+$e#=1=g-#f-`iN% zL2Ra{sA^B$))KUl&GW}cj-NdB!i9r}ju`*!`44v?YC(b{0-iQ!yr-KR8(uG^FBq}_ z^dEuL=eW~CUCvxM z5O`n=Szs>-+WMG$3F}56purn)_E#7^H*b!#dnTTfj+B6k8sSqp7PWd%qh!8W<1oo` z+5&frwcqY4x@9!`HkLdl{SAS14TtA$J03ZDRI)4M)yj$=@uEYPt2pa1Y20~_9mpoS zN*qdzj{>IXveIdF?SxHl4(EIgkP)aYm^R29)z-$toyR-JR!*3lga-5kY>>k-m-!3?r(p!vUbetICTAa*v7_#joWwbU%P(m`t|!a zu6ugkN;7tWU>)?qya&L?O+z3X`GG_R8CP`zY79hTV)u3Vam?&VTi7^HMQT&ib1>7s z)F`1e4QMw7xfDwp%pBa=TL1c^zn?sJ{_<L2tx|>^o>5O$tt*{b<4mIEV+v3RajmtBHDk!=7Zv@ex|FN*7^!47 z*3XkG0(eRVz_aZ8*>+S6Dvt!V@~YNPQOhP38qEQZ&g7GT^vSb)yS7kf6jbMW^!MYyC^Ot-H&O#V_o! z;y{O_LDtaSUzO^OB?OL{uVK^NI|`2$PSn}Kx=61+VN{t8Y8RXhc~93jCE9dKrpy~y zmM!oeJ`0{|aB{1(GcTFR$p<_!He!>jRfBj8CBfeg2hKUV8J* ziz4&j%FYuL#)E>b+Pt*s;C_3#{NvAmzWU`??yGnoYF?7Ogi16-xVrz^?8S~;gZ?2# zrWE4{O(XbQtWD=ty=oS3oIxBYJFN>dOnstvFxs;hL} zhYS>!Q);q%SxgPGijGFEWdP{pV(|&i^vFl^a4^l-JdT}@SRZ&*qeJzbo2yg9B_0;{ zA)3B!7uI3Hc{BGtM^F~H^5-?SWP_em?Ep+J(D z4tR7VO+fW!P{qbIQe=;^z%=y-JgjLe9kV!I&AYmKv^_>#&Q!{lPw72qsZh|HSsckk z9KDg|9re$KYS%kT`x01*pd``hF$4-cotKRpsZ8c%%@V$4$wbym@i}5s!SjNi5YFz; zo;}*Wv$=KW`pq9!SDN^j8h(116^%GWV*iV;zxL9*KNHSRwx0?xqnIo{=3guvtuni= ztS20H?%%laczf$`cTWj0H%d#@jSy&f$~}j(7sg}8G9(jcBu~H;PTwAFdb+*7ZZef4 z7D>US02Of})V`CaPk8_UE}Y0!fb3bUkXI^NR=+&;00Vp0FpUl10c6c0^(^XG@dWTu z704f-oXFn*E`Gz-SN}LX9vUgXkxKioDwxX#(B&~ zg27173a)$kZ85?WylgMDaC!QCdvnv1024r!3FpOo(zASF{Nl!ow~C#5@x|L$uX=*s zm@csEb;Oxlh&c-*Ep@Q7YK>HV0Y8qgK?uQ)NExiYOP2+C{t*!87`AaWc z{qozZ-+pOQW@~GcJuL{bH&gcniMuCnJ`d69AP%!UAf3D3PbqC?K}qLl)Z!`0mV~L4 z?3n~Ah>aWlglfifB8>BsYROjoB3h)9v9qvZyc;@HT-n$MG-xO;2NryBA9^yq(-i0Q~dh~3`va{HeldeK0n0E5O+!YC zz^^9b`BWv-H_pq9gbU=ZkAx$E(@D=LYReYrtB88?CwByGOs3xA6*&TxV`{|ZZ>fBU z7hX9}K*oAOaUjW82}%7*4AqgZDJKLfO~|>qnSPk^eR2$5@Xs$7Q3&fxlk9c+$gTK@ zgwE_}JoLtgo1UB*8||TWBEUNE+%_-X=6LC)%e|lS!i6J8j@pQ=wHb6Gz4v^1V$#;j zpMUk~U;g5ilcLajjs}myr_3!PQBuAEt^7# zYK;;QO@4){8xVqEew{_HBvj& z4+Eb{b+pLP#ag%E_M>DAT_gtjmIIQqm?sh<`m1&H%7Ph*0TT!*1O$ufM~|#b-U!ut z-QCOA_jbpvEWU5Q2P5_$UbRNZx+eF>R*&ZSB|!O-7>d>9OBVGDmxC)0OwiuEt^GozWaA@ztmBTTdwLg!0kANbonG~_kOEDd%eY#ReMu59oJp`WE&U4 zsGfz#0ee$`WD3if?I^D%PDZHr?(uc612!eR`)G%s6N3i%K*q>R^`~0}RK%wny9CKt zW;AxnOQw&UJavkR4K&10EgMhF0{HNgeKyPTc>3IKoe*4{2%WrhsFk*!dcNh-pJrs zAdMrU&*vgctp$F?@?6Y$T!{mngROP`Kk zIQ#Cu{$6%6!SLjXXAA~I+Gg@nX8oG%h*&#!KJJCOjapE|U{v&{LEp$NfE2E0F~M81 zZ9I};f)oe~WP5LZQm_dYjZ^E+`Dr^Y?MIJhSit zAlzIQ8wIm6j(sFcJ_~rJ>yROfl;)05(M9fJ&RL>DKkH4?Kg-QBkZU9R(iho-X)!D` zLrS9_*7%zG|dG4&owJyE&`i&odxbx$W z=2N_LdUt2b^X>)$lju`_$d-0fdaOWSs$pd?8SfBG8M35G-+cjhX)RCa60D6=$(nH} zQ(1+RpH4Z^?11DTHT&^*xTgOQ*6Ya)QkM34SqF_5Fj7yd&(p569e ziMQm#9FUoM5=GMr&gL1zH~QI)KzYA|WnePSpmJBCPl&|Vq&BzeTO)}wMMOhu>elm7 zol@!BXu;eADJejnT(GJxPE%UW1Y<}9!-Q|j)-RhE@e?f5th#DI3^-VoG(Cgf#PLI&+)iumahKr zj)PwZ=%lZ&_^8e#9?PO*f=4!dIDV!5K%&H`{A3HYv2(2vE{zmqgSqq6{PPQPyzEC3Yh zGYC+KCS-Dh(R#tZdkE|?juG=LlU4hMiqg?^wj;#r#^ul^Lfu?0Jp`QP!e&q1xegOO zBrMb%x%I9=v>3t3B&$EobsMUWrtOEyS+hEr77)g7@SUiq2Tv&0u#zZcnk4J6DkRFE z5L^sc3=oxl=QsH!ctTX(!jXNg70-9_Xr|A>f~_{RGhCwyRb=!k>qi<^B=8<|Vmv8R}6<9-zgz%FVUH)xACpbH0*6 zJeKR!J+SZlufH)8IDh%l$@Aw{*N*A&>h(67wIYZEg$R?3f%fmafBVkHz5A#pagovG z@}vI1?&GzyXU|-G(V7;U^Cb01#X<5`FjYFOoc7T6*7|0Ok6Z3VJP4&H3uKQSJ$cSk zt=|3oSTv5B|Z?6I}&ZWK8xJs?}SERI;``LIV3^sa;2zXnBk*RCk1`@+NO zYIF>FMe?9Zl+cEm4UxQc_3+^a_W8whs}?^;3r%NubDIW>H52k=8TM%`S2&sDvl9x+ z%Zg*CG5Dgx@L4@}%zcFC`yN@fl%cZr_ng!kk1 zE%zK?-f6f5RsOc)A%LxIYVJ(}$ECmxx%9@Tn-pl}HI`u0{}dIX_YE)33dgTKK{p5r6W34bX;2UQ-BC(4JJA#GE~4dbG8eZGj~2saO6^- zzD+T}w6Cx-n2|g5%SZl3Oq4^69<4d8o#1g#o7Bp0x|E%q4hw?!!(Kq2!Aq8EfT0OZ z0~d=^b?lF&fQ`j$=KE}i!*Io(Q)<;M#hcO_p_=>~vbB$v>r`x*p^=gf`EUlGq=yWx zl10hk7eh@(Fa%?n3njKmc!tX#rsb_%cI;6`+kQa5!h(QrLJU1GXg zmG%4gKm5~wKHAuP=QqDG4EF4}`9nxPwF0kN2hW|qaQe)d?YG|9xO4mJci-K)diCz@ z>${JF`>hK(qQe*r@$>P9YwMmwfbIk&t z)CnyoB`#Fz7fXkORV?UO>S&BJO~;^>H^BgS$udrNsd_k~TWGa2nXO1$@=hc98Yeg{;h$?v?!z15 zFheVR28go7J(3NX(N z=W;}you7oL44&i~N(^7Y37MrAgZ)f1XlqbWw&KK~Hy;V25F6*}e6~xXMH-b|kBK%E z>v&HTO*xFBRqtoMVl>%|o<$R_5o}n7U{C;OZU?Yqy*$Kb9ynQ8u?4AdMXf}e!pY-> zsid&!$Eh=CY(aSO@{4C)xTN`a2U$g-y5;qJWuVfFh?aT0$Z6y5oqzoOgKs|h)P98S z0#tk|#uSN^&T8v_Dzj)QMxO{YDmm!NPh@sJq1C3HSfv4M!N$vmp_K_ney zVHo%x;fX=eOuot#xrBP-i^KvzfJ%wpU1mO4%Vl7?a_ETtrB5CAo=cB`%G35IesKMk zWzs>6_QalO!YAnJ%#Y}rxKM^u9@I5u+05?<7j-gF&$W!%Y2~I5S|W(%_;y}b!G@oC zPQa4Q$wYDVd!Z7AGf7#r@BID`=Ptca|HMoed9upl+4BKp zxECq8bkG!QmK53jX8p?NU#;J|WA~fEl6;(&(xrh|=kz(uSW34=GdAGl{|c0m<(N+S z;RaQ5snzGw>f}}*r6FXKqi6*;w9;_Ao%`j3PE$WR%2n5aGPTuX7ah+ImPx*4tTrqs z6)K()RYCIUgB#Z$+_>@0(W7f;PoKMR@!X3SkDoqwMAzBO_6ohZgHEGP#Nn2Jdy&G{ zmh~#J8<#+6+EN|r=bphhd+9s@VY{r#$pr#ufn~IDmu>%n-K`y4F-ByBLu4=#qrCa) zv9)6-PoF`-9zQvsK|sz*B?sk3t|;!1K4rhMm98l7;}tFk3dt*< zuaw%RQ_vL`Lc7EkU?4$d96tzuV&^M*+(^A8tNgTrm2eOr)otQy1>oK5Uw!=P$#dt9 zoH((yy`%p%&K4sU%R>ckQ}?33d9qeFbf>Is7xr7Qc7CL>5G0P|fNCqZ2b&3c&E4 zThTd1mt`xwa8s3MY zl+lx3;~jizsz|zga^^G0kn`sRi`CLVrEbz&ZC$^Y^r|3>?>pwpDtDyVEAOE;^{=7a>uWAelL1uCCt}J@==#gWuy>jlw%ieQ$ z{ktE3{QC2IKmKUW&a2_sB~NCc=?74tXl0O65d>tjX6Ca}5>dV82ZNC$JkqrAU=!kM zi%JM1{gUc3q(*dHC*wgVJ31pU$LvjGxh&fuI-&pq3UxV_vN=U{h>>i5hdoB&^^=2P z>yZCvjFFI>1e^dJj~#YxoK&O-?BwQ|*AHA;H0Z}GM*>Mf!GZdkdBy>!k*HUYKyey& z2)1mZvoTHnpYniOz!k1(zmUyum-Jn+5ZxoyH>b=SgC4`?L`G8l(-nb4!D22`VE`B- z6sL=wNVasem7gI>)TiL&Mli)ErsHgCRgXH&r%zFq#EO^Vq+FlCnPzlKfXE55QhD05 z=qr&|%Pf6n5-T`H!t`>Y0aGv)4sn|YbwY*+Q+m$kD-(a+DFY5mS9@`b zasOGd7=D@0XN5{*1%X%EYdMX1_8+mT$+YXabEnRqK6>nwo9E`IH!Re>+RJWsXP@=N zgIsP~vI_gLRF*Z371pU%zcN2%MzW<+I_7&0izb&{1PA5!==of%QmxP#6+!2ER03{{ zysU)Lji1hKY0xwToPerWBeHJok0hxap?<~{bEC0&xaP9;{;TJDcD|3KVJFqo39H2PC>*pWX3791csDUq_BUVlrSt$g+>$zF=B8@IdE4MI?dMC zQUTAU2E0X*li?Gx>fy22fQUI|QXPyY@a!aF>e~UunrURGgHG@4kJz-kfaxYv0G^D%^SEiG|~5 z&Kx^&a&CV%;;W%67gHhQXbMIX+sRuSqlTlzh=LD?wO zPVvkN_E8r0^VZ1y`@9Ok>&6sGE=DfvbcB+C66==JEKXkV`o9Dqs;Kwqaj|pk^r^K| zr|w<5)=U{ti^lN-#fpn3?g03jR4VyL$DjNGaniIKj(w{RubjSg$?7TPSHd7RIA+T{ z^uVM08ylM&XV#9Oq2tmr$|BfdPyso;=>#ymkGSr)6xwg*Ylshf}?Q#3Q!5 zG=nWsns6>wz=cc`-bt@q9-T{v2ht=oPljL_DlKYJ)Phy*Lgkj;Wu)NOa+N$MC89LY z@u5h~=`7gSU3_l&y?5Hy<$va4r2@tSPM=cy`D}TNf^0vRS7aKk~<(ZATbP za4cFUf6}9LSq((Bt4VEC=D^U%m>o3Wp!{b&E1=0iM%wAxLqXe6Mp{XhLR=4B~Lh zADZuIz@jYJR0Z+e9BvvmKF%)&5N-NDOHpEur)S)HWd%S}wTVx#VTEa8&N)}cw57MF zPq(%nc?jn0QLhfXXyY>X>v)}g#AN2NiN~DYFbzCXdqZGQaS$EP~ z{#0+-$$W;9sYw8(@@=u;aIZ&_Ok*EGQ}Z#dpif&LY~ptW&zlmys*C#tGuF=KjkWRt z3+r-|aygMQ0~Dxxgl6~Y=O6#w)>$4xyZF*8w)=7;zzgco1)-Q8culPwv_9F^Z7;s@ z%C&F5`Tn0@+_`$KEt^fhbT=krAe=`4yBtPK=knhyLYq45eaXF@^a~b6fZff6B@sBC zH)ZxE3*?)n3Qo0*s9m9)+Uks^h0p-r6~8eYoxu`rI0nwh)wkiy0m>%{F#=VzMf=$W zE19|&nQkbu1Ap30_{Usvwwy3XwDeIbH^nMPpid8uI@iTUMoqWm$c2B^2^1oTX8h5x z(uyme4EYBb8!*E8+lUCHNA7HplOJ$wI+^T z(qwJhMM(%@HYD7->md@f#&e4;R{#%{ZWQ+8xD-dMLFs?q$}O>DHBR1T3dA-VyIwrG zw)XaW@4x>3dqPNW%E?;H$2%DsGt@2a(S@t6@$!`?cWz#B)9t%YK6!A*Vuoie65vjH z5`zf*6Dk)Wl>h$aNn@e$2p5wYypn%*EX5>z5$Q`f7T?hewLnCL(aK9TJN>0X!>BT8 z-WQ|EN_G6=JROld&4JW!4#|w%j!A?bZ_uKgxOmZxWA7XH<}x5`Kk_n}n|E&9+_-&j z=h6DOKSrio$%9P2rfd!D`jMj*zJAMjGz3m^jvC60YmVubvfYbRENxiNjRgq%EN}>^CVRuiohDT*(1&3 z916%**@PfnC}#wDvGiOQ|H#P`zxd-H4jo(h;xB*Ox_9r+4_9pR>ZLz#{rY{|uXJ-( zMxMKCghq={C{awHLl&TB!(l~Pn1I79CD3?yTM!YdB!&asvr3nwZ%n<^aJQb26 zj1~t|I?a-$lAJGb1?*urzYae0x?49{+=}15vw8RSjqkoctXq2g_=(e}+LH6qba^K%)YioIxi9j@RVZhSXG5JWW2){pnF?x-KI2n&s3a-ROsbXV zXD?jv{)FB2O%GJMN1=KH;{w${c;CkQgWb)|LkBN;$*tj0GyTAm>TI;Txw*OVzy|;K zZrwIqT)%hMxYAgFR9Izrrd#~ijr~B9Am-e7Y&uQcOMbv)uy%<^e8!(YIbEI(&`DyE zG(&>FnR~rVLJb(VgiIet_ z^(e@$7SD@a?%ck2>*o5c+gpzwJ+`N&(T|t!X$N>WBhM@g>O`wCCSJi&Jg3Yk7HEoc zwdr#ejf=L)QUo$FH^|4j7P1wESZL&Ja#WQ`T{-?{tpyYJY8G)Y=3h)=9y)m9^x2cYJ9FWc*T4MegYQ22 z*egm`_OGIwlmsA1l8op@Kpvyv<-4OcGfZ$0P6jO3C{tf>&-qKFlo%9JV>*<^4lL16 z#v}ANaY)o4;hfLSlvbq@qt5#Xvxh?IdkhT*qI5P#$Oe&%9gEn63llBAhb~3CTYoB0aj)4eN_zCn&T4I$iD9mvB)@aNiUWlG$Qub6ggQNb{0W zd_*j?BIh7f2$CTN;swvqi>zp7@t9A+{`NEeLE+%fSlvnt+&Q15Od?15%sLt_JP@2?J!_CbB%C)7PB2sI#E9h864%ODK`Fve!w@!KP_3nkz_3Hn)yGEpHge(m?r*$ z`MDQfdDXj?yovDPg9kr;_XCYAXYyL5eY-Ym)Et^9;pL>SiH#2}Fe`oOSEP>rSpnAd zLgGf3VoUl8X+^+anMM7&RD*$_>$B4lMyPKESuP>GeG5ch6kU|#i_{)Il~z&H?(9U2 zA*&N}meG@>5kyX*sK3&>QAat(kfCKjbtl0ruEJS}mtc}OZxkmb%h(aqH|QN)J^IeC zerrX`r+@zIgWI_r-S@a$93aGq=6s^Wj|xF@+_`r3)(_Vp zWg!3=%3A?JfeC~(jY7hwesZUfKTbK9m*fyGaf;fgg-H|?ajDS(Vnwch@gTi0Sn|R+ zM8+9Zk!+HCTyEejAE?Bf@$D1UxF$^6z_2^jked9eajXv{nZVio<~l$_!EDqKf*dw; z>8*LYJL@~^>-Vl+^(^_}wN<+7{aCh}59RpCgQA^_$#>?`i!Z$VvXvq7w=y&Dt3PSe zPhDc&I4X@@Ji7ht`J+c$&vylAV>bbpl+C|cRPxkYnopfS=cUOrdrJD6BJ_2KL=7B$ zq@}4@Z}Dr`;1B?1BBKZ&zy!1xtop4oRLaD42Wl6?64By|X~~og8;lpC{c$x6%O73? zOh{ck*tuBX<4>LdFM4!RZQ~=+Kp9Em4fA~7bw=Ucb;m1+TXxVS#Vq$pZ(y$2O8n?r zr)F4{y=vsrND<=c-?z0+9Thy%{U`pJfWGQ^fXVB?y1i+pUyD~8h}}}uyTG2BTNMm$ zjmn%Odo;#vz(qPe7iad{SLw*flP6D|@d~c|bwaD9Q{k0A?ruENGlqk%u|CSq_PXbH z{o7pMSif^`^WN>v2OGTv`f+bx6As>bTjwiH5{Fjo0eJRKf_!#zHrySh6Tf*>udZXkz_M(@swYU|G69t?p@rwdDH$a zyq2gVi;^lO9i!R+Kmjl6I(7cM7m&}J&L&*RmG_hfhu1U1I;Oh${db@J<*zrt{ra&d z(Dv^;d-GfD+luPKxwGdk zp1b_wxffr&`1)(0+q`q@j+e*XG_bt6e((Oywr5>Db89n(+==|rBwf9m?!r#roDX52 zgC!DCYDbob2!s{OyF}MQUVPR?fkusB<3{K6qgtszqGq~5KR!8->!s|zP-!HX3fwW7 z!hN4N#c0b606zPF|NP_E-<*H(rHe1Uc<$oGqbE;V1IA=V2!R1!@$E%^D~Hd&aA9TT zU-s=j^WreW1j%Cw03es9;?7>>7pVoGtds#t;pAmO>?P#@DR>o6wqRK#trQ~DLl{q; zE~6$Kg)wvE=@Li5Ix1EJAO9zdIz4ppaf-WfH0loX0n_E!RiXdc8}Y7p#t+n{M;rSg zoq;A-MYB_jIkbRHmrWVUj<2G@kOlc7)e`Q5!x+=IvVI?xn|wV1Gcr&6FydjDEaa|D zS*WVAn46?#c`%momq^Q{oG}4Kml;fULRVxo85N5GKQS+NFRH-?(lt|at%7q65YF{~ zfu+!|nL)1VK1ULz>K?d;QB!t|mm0ABYe8|A8}A> z&YVe=9np5w$|(IvEpq*mj2m!;Nk}0SWT@qcBWzL)jD%p!pbbpdVVzwqKq$IqTNjdbhA^&dX`@WwaaY}|Wb&eqDN9x^+A_@|^_ zuM7}LGoD85pqT{~OAmlQf!6hGM}!KFk?EaD)J~GIzK@D6z22Gj4rG_=)m9`R`~%xU zCd_=pe$QR%i^#)cbvh1d28F&DV{2EJkO->oM9*@ZX6hJb!kLIzm7q~=;vyet91vpR zCY@q{(&!gw2Xr;ho)sL?{5Pe z<_#X7p$7+|r{uw>UZ=XdQ}qsUB9?A*Daru&(3-i|)s|OOsnn^IAX{#J7We$jwbGl& zi$fUs>tla&?FJ!-k9i3|J@&yeGF>iE11$)}%3<^4@u;tqzme2P^>K9c{a06^)_jrz z+0EaK2-CD>-Ds~IIk9FHqE&sBRS3`AFkeTe#ZHz499hLpYz*6)pJ|Tu`)YIS&f*rR z-S;1C@5pG~T1*kv&a{Z`?mVW)k;5%gCq#mvG?rg7PtbAe{g2ny*3Q22(v`2ivSF6H z8Oq0`RBBa;+nWb|VsO1fBT=>d{FJ$nUOW*RG` z(og?k>XOsW_!~evW)mf}i)WQfs9eV=wk0j)8W&Wos(8xw>B2=gnXkE2CnMl^3g$eE z;)R`~MV&ZHM4cK*o<6;O?MC;+l3ni&nQj&|Z=IYy~d9Z+86cJSKs{2uU~xa4VkLF3dq@jJTr}G8oHSq z8ha%l8O#u;sqX{Mp3?-^fLP|KYd}0z8&&2Q=@d%D$-k?wEe(ipA#l`umA)0DzG&(~ z8M-#2;Ll)ng2n^^CTT0tH>X5wF!J_Dg)N4t`<59qrgELnCcY=}lJLfr&`r47q{4RD z^40gJk9W3j-FkHM=IvwOobckF%a_i+`11J|F2DHd>z7`CW8=Yn(>HgnU%P$%nzuc= zA)xo!Q}`ylG!6}{ldx4{186GMSBwik=?(_IO^?w{jc(-`fsA}3V=WBLGi3KuD4Px( zfo|=45~VK95V)zsIt!P?#|PLr_P*v^%p>?txloSdwb z-hN0gPAHtjA|vB6U2anLm`aNcIZLf5iys0^1^*{z)$!v)wOl1*bcANe7Veu`Ez~h<(|+zj;sEr~aNTLm~1hU8v?hUD9Wu zi(|K54;?vl`HfePojkeS1bN-7tfu*0F+u9-#+%oFymIxMZ|`5fq4!3elDfXX*I}l2 z(8QPFqK~3*5rqCm$S`DPT^i#}(e~LHXp@R;z|J)lPE?m<1G5Y8RbbCRUTbNlY)VnH zf-w#OeFol{S=-TQy1A}V!A1Azd#YJFTRD94{Hy&V`~Uzz07*naR7)?t@y3PMUpwXY z=*d$i39f$q-B%xfboa`4kM2K^wOgdHdOT?~J)}tzwn;D42wFQj$g*7tVQd>r87{qp zVVq{SmWzthJ+I5I)$4rSl<|Ye&^hYkKgj@31ar?ssBqdABjcl??F3)T zwUNmqLL^Y8h=OFD4or-bb28ua+NX98u0xU8j74}R)w%6-PYNV+ z0lEhxqRS?jdxnf=fml%aju7nKX&)VQLH&du$59~p^Pa&mrOVPKlwgJ;ArgcM^O?>f zw-B6r17r!3(bu1`tIy70UN9sCTH%?yC#yd*?+41dtC8@((-UJ#RMZ&^k&wn)c}o~A zpdt~8Cbv_~_XKXoz^Bi?|LW^&1^`DrEpTG(>?u0}*-U12&0Oj+8+v*>+lt3*_B}Nc z+gyLN^=JdsIB>Jc0)dSdNo@dn?9>Teb0_?Af(e-v8Ke?zX0J?Vn?4)yM~fOq@&|nx z>gbU*J?5$7!lFF-DO7<)&tkF!724@H{#ci_Ky*`{%trY343Yk^ zHLInKtXMn^8?L|r{BGDZNI81aHUOs{-nx~|^E7s%TR4*Q`)ki$*#j2pG zfzpXGZm@py@Uhh+7hino#Fr7pu$itfAd91})@!I0 znMEptFMJS)F@lpy3x$P8?txh!sb9z5E8 z>_s#d9A%(Ukya#{A0J$xNw8Xo9*b+{Lo7x;8OykQqINKr6gQ@G?8?Ai!bf!|05U*^ z2d6&Rn@Ly)aM<`_p{nhtB38U-Wrg*gnkHXR0)f6nrm81MOca8CNIIX&9NEyd3aOEq z)Sfju45Z|QR*nd{l$%^LLZPB)5+a=X-h<|@JOOou?_R%o_v($qpZxRK`O|Ow;{7*& z`HR!%FP%Jl-r}oQ9;{#g?#lJ=zq)tL<~i0JYTJ7^2{?pZE@INP?3wc~zWBy3etG8NrFu5zT9CHP_sLVq^VHe%Hy%6y z-?$lV0(k!92nL7KKj9GpoF|RVNR+?S+9c1SxBf;JcQVtI5Pb3>l$~$XH({l8Dhz0` zTRLcz2MiXG15$kQ!eohjHYHI~60jzDk#+2@bdC|C`A|FRp&T&N!${v?55{EiBf?$m zxCXI$l_YTF$9wm3OGGQGg02z?O0~+BY3j$-d?^s?y|G(}EIpL0SL^7E(j6$|TQt!M zo)n~CC90RF`*8nyvS(~kpHAtXqAEY8RxfEuA`CL$6_y!X{3}bnv28S#iBeCgeHFI>8;E3>=v=-O9b*;DV^ z&;Rl0{(Y^22~Mhw*A}UZ2QEH9)9}QWV%Y;w9`SH%=Ji{*{6{I}(B-iSmG?a!wsS(ZODeQ@>+S zb*kA&dC%kBXk@CYOd^zU6&m;>;W8d$<-FYY3iiCnp%-3##SR`{{{7Uq|M+b8;ri_# zuKaalb9;0B?O*-s@X@1>cP#3yHqvJKwfSj#@Zj3X(?{0U9^JdAJ0#sVTZS%;Q%FfB z=R}kEo4Ac#h&01JEzW3}rGle_!l1)ZeGjHm6K*4~@Cs(v!zbkynFg_C=OXeD6##Fe)zML@~M#qhdnY+H-A~Ii#6^-k1giSw7 z=GgbFeSNkb-F<#<|23g?V8zOIZ{k{8J9cXA^l?vPo1FFr+2dzUdo`Oas*swg@x=r3 z5^cVlUG8*wlK`gTx`>@Dj%|)J;aLXo_PVG0J>X%P5Sd_6f$^$FKJK_3Jh*zyP5=i@ zNK>JX8yLcKfGkv{i~=#nvt(9O^B=edn&QD^Dq&T1Mo3p>Cv+DWn6^Q@^9Ddg;ex-q zH ziSHxRC*VNO*010G?7@H9J?ZuLe|7nd*N+}MZqqV%!Y{u1%AG6UUj6Rd zyM_T<+lM5K0xmn}9M}zqTq~6*j$P9bvp0dWQ^tqts>~zA5}@I2_Uf! zxu@ve`s!yIcJfo*k%f}rgImbXy%XuSWyh%2c}SaFxNO|~@u5RUyB!;qC(rI&yLSKD zjr(`*{o()k-_Cl->T!lB9Wcx)|2lDl188OH7^qaHB2|t;YHo_UBPygV)fO!WR9~IN zcXAE7fjy<;G5CxZI7-E=8bV4$tW=2yPWeOr*C2Q@>*TYz(tj8>Lt@ZU(2w8bqEUr@ zme0}sIzj4aW^imW&4@JO$9HIsJ8<)4C?HCN)2>z4=}C%{j+}^o7Ic!7Kx#an*)-Av z$pp`s84v?u`t2#$)KelLfH|>?BPUSEEk~TxNXxXF?-Cu!>jCaXP34>$VUS8KCn7M} zIzmA(Eo*#KUSepXw0I*<=1dp@40y?d=Ynq(d9~{Wd8ZR+Q6wM-l)SC-s_kLP=}Ntp zNOU$FJDfz3yv(_94i>&i|3$?`&%v*8UFtjK-6J)h5tuxbB+`q&Q{JDnOzZJd8U@36 zCZNc;q&$KQRQgoeSM8=Ta+5k!lX}Q&C_E>1rSi!)! za!d%_z_@QFj>$r*r*5G?eQe_K+^a9Y@Y)-f-+1Hf#fzR9dbGK5{qt{beDmG4AHI8d z^R_z?-cF>L<{Ys3VXcrKLd6{xFr2@IBtK*bFy=OsmX?m~;w;V&J9MiDVo%IZ~%-wiA_F5mPC|`CGQjChu!A~&zw2)-oO0u*y-b6 zfAq=v&D*;5AN@c7!>dx?`ps`VhQ((3HEAFg2E1413)U+x&_!b^bBe;@G`wdk450)naF)7?A4dtQf-CnOXcvrW8&vqJKZD7w&EyuNaQDpGu4X~$BVf$n>LiwcF<0{p0qHI)~bK)~zL zes}AAwY9Nf z!*aL6Yy-8ivF=5`JDZzsuMoppkkr9#L2bZGX||(G=sBuPh;q4Hj01?>+T+BZ$n(dS zC0vw1*j(!190k}=(VxL6kerORf7t}gwJOtlfP?cjv*l`5%22tiNWsHzUf)-4%G$VA zpz zzs(37I%pZ3&1=5->>po${PE`9JDoeUa_*Iv-~RQlUw-RN4~u&zRDD8jLLtdlb#d~4e%*Oj`rt1^!%{xrXFAW`s;rY|uL;F^j+lm1!27mPODO7-1pNf2zjLI6uPc&+Lu>Be{B*u) zS{FH2fjY6O4r@od##skwP8Ue?sFJIhKim^MKh6)*FMkxvFzpmc^4UZqpvWXe1e4i$ z1-rRE=93HZpG4`e?>QF&kg2amaT8M$ z7rLATtTj^shkVrd+;`Mq&{A%0pV%Q=wZI`&44kQpo9x1duu1KDlh4IhQ%ChT36>FJ zktjO49bv`dStfa^2MND$zC0d`Hm6_Oro6b zK*}tUoK29`IbW*cc!NUBoZ<&rj(bW(Q4Fd1$Cx#Xv1%*@fY;`SrM?{1YIBYt~Sg4&E-AkA-w<$JzcJ zVaqsC^5d#zS_Zf6i8TwyPaHpe+A9T)oH}L7(p-ZbHVVeJyr69Jr$a4NLjncZhrX*| zasrSp^&sQw!9(|N-~Qmg|F>)3epe@9fs&QlxFEk(;-ZuY)T|;*~I!dL!HbfyOK+Rqz4UQ z+rmcuiO-tF>UF4)(~~ED(#yG!!>aLiwIm|pW+pgt%>h&`B=bkSe03Zb_E`F84Svfe z@Q(>{N-h)rWFuEjMo6R+HAxg`=E@qBp82r28o`vII39XXmG4{qz+z`IiHn%eQ7j8V zF-Kn(u^syA+S25dKDqYgmsY7_gXDsjj9Vy_3g{$3WY)S~!#o^(MUPX6f;;p2R^imyiva|-S&5iEUPbc$gJjBs=et%VlHIQZQD6|NPwN*kX( zd%nG;aeQExD&v}#Si50AA( z47RuLeE0q4{rl?=*KKmS^=Q+={YQ`1cOQ92SR1Z%mx10nHc)kqq`Q!fGki+84P=IH zUNRD;Q9>(LTbzV6lWAvKqoaQIisnX325cUtxv9yZ%k)nd5KnRLf>HhyY(9gKU1Ah$ zwI&j>sdy}nW_pKhnG2YB%c^w)613qFCZy9oPa`pOu*l0ZS{W1vr^h;hOgBmuhQm(a z3HCMH)vp}h-ri!SH{O2t)wkXPh+z`V6g;q+F>Mv_<=;R2{`1eRW0z|@1pLD5Z@lyS z-(7g=WuWe~=%0Jea4?q8B0&P85nweVeU{2Fr_#vfY{isG#;!mu>5y<7du5#kTZFO< zt=KCj3;hbzg~PK>erF>9CEpm$?@)nP-{M?~wYCeuW1n8SgkzQFa))7^h)%=GN7wTR ztR0QVI+0Zu<>~>C6FY$p_IXiqMmz^l2_SW7GMUU;tSI1pe8R$GCvG0z`2M@guf1{c zm6y)G^0H0JPG7#fcHTRuFMs{%Cs)4s=g#`(%KjrsFqDQU0DYD)C3cbW;#+UN_iz7d z%$9odCcn}LzD-&wINIcACNk!J|4hP!amyVe#eqesVYL5VU$ABWtMvzVzUiZ~T-&!#rK6m&>uG109=vhu7Szl;ks}GCAOZ9{b2kv)A8CbSO-sh>XQ^El?3uHU z(a6WADH#)ulsU$tfmpTK^u`9%bqMTQG8Xm*k;AqQ`1Qa4hh5v(?>+F6FKdM*y&`Y| zk9dYcSYXqtn|=G;`#XN>lt<`Jdy4M#sbj}a83tI*st&RFzEwkY(08}${Ttdjt1th| zR%f&b#D3lpb(>E9*QHCua7&K7uKynR-;+`WJ8D2>Y}D@WOPQHnQ*Ds(I7f>R2!m; z;FZa%8hGndS-)E|@Rp^PUw!n^l}|r4@%i1y{~u9r8Y6d_-}hBjcU5=wNwS;wO%BPK z;ha0O$E}0ATWdgG2kHaOWU#G_&_8hjt|L- zrPadHYIkRboEZ)|oZ)>p$!6b29bHF0pYPM$RFhTz|8xBA-|sp9&;N_9)s-`Ey>sG~ z^CQhBk)1z)@DL`?H*(D7+aG*z{M7Mh_a5B;@&*rH^XOCr2gb4kLW>2B?Mw_#J*3G) zP-;YB`6(2u75h41(2JEt48KcfYhO{rI?FgQ%-|5)JA>_Qc0*Oi8k|oW z_OVZAg`x-9rbmsjfd&3ihUprn!qa?cSc^iyNS<&t3+rpIc3i2b#XxVQtr7u;Cu#-2 zjFq67R3?Lbs|tz#pwO|ijhN<-sW7%xyC6b|crz6(OLq<7Mi|CydvUaT53XN(c;^;d z)j7n6S9)_eAhY9~l+UZO>g*}Y33D(VA%iqGuSQ-I7$`=xb`!VP2hG!93$L z%ER{x!PYU4rQ5poJmLh)5TWe~I5KP$x(Wv#5|(&IfTLrRvopvLz=(wc(#g|#CEv(A z;B5XOy%$Y5`iX}z@MgiPNB@1h+u5q|J_mNJ@wzjT*bji&OoR{G4 zLo^eBYnPx33q@5!os2c8-aaiV8 zE7nx_i_KcWRCrdbKrlcCu0#Z`?;v)h#rx>U&KuxLIOQZ+C0=XxvMf8o)-ak@s z$sKj59M*lTez#b^^vflg2enssbpNDsAyk&9$vHV`Ir{zp32 z6##6hUvGc}l%f+WfzMI`;?JhQu~WU)q}RQ*ymo8l60ecwv0%nmyx5UfZ_OM!_~xD& zULeTkD_;A^5mjsi1h_4m91AX7vN-RuAyW*-k51c(1iITrEQK)AsV0-@p(}=H0G2pz z1Chwy4w<}Zlw||X%$;K}2qM;)X)Ab7#`@aI;^Oj)<*oJA*5*dP!=r!%9p=@63a3sF z2TqN!cKFn(L&uJ~ykG|$wNRkShRSxE;V8COo z040a&kfK#`Bf*r*b-Ecy)YdQUTCF@JM6tjjE~QnYu=G%w3!;*KXdQrJVP23OBQd^! z6w`8D!DPk8j}=1|#SJYo2oO7PViU6Yu6uYFxr5t4Kr8CI<3vRP0+~XkD)Wtmw20j{ z6TqZP4UHv0EzCdBfE=kvX8=0*mIX7Zpg?6f_ASN!B^UWhgF`vKONIg!I~`ZExeg|C z$sjU1sX!&JL@Xtgkzaq2P1q2@ODuB9t>TJBiqQm>Y|P9xn(zn+H6P(XyNV611s|h0 zge>j0 z%`HEFcC6dt$ss0K07*Ui>a6LVe)YnUGpBi<2=3Qy^8#)Jr+^JzsFgSlGQXJkuW|+- zeFbL!c|h0}OH=B%JG3rHB7uO)KrnvkXhq>RX`)n{F6QVRS}F>VDO3bb45{5);{?*> z6`L;}==a^X)I%=x8+B3IZaNBQ-+tr#yI*G<(q=ZfTA{VR^V!ueE`7AUw9J|4Gzf5J z2tf`qV_3s3qs0iv<{7D5K+9CT+#*Fg7?iNOT|e=fKoJK{K#_jS2vc^*u={Q&TS)R4 z=^-wmnS{N?NCc!&Xp^!1A}mco_T=G$cPHe8ST1~~nFW&wFe?!|qG|L*lKfTS6q^wRJqDA~55 zb=!As17htlp?ACiAR){0a1S-nkfe}87mb;seznzR0k?N(Zur5u%G>8^2c{}}CkA_( z$a~oC)s|YLw-&mWZq%;a?=3D@IveB7S>AXBUo1@vGGVfaHl7FEcyADbNkt$CLuN>D z7#qPD-=t7O(SB7r;tkCfP+|1XokT2mrGCjsOcb6_0UpE0b4vsgfd&)|0s=I20npmW zrCd@>zhPdjH{5P>qWnhs!I0uE0EQ4O*7I`Wah?(64YN)5@w#9taHpvQhM=F}qwNzH4Nh_A#kK<8<&BaI2z+|e zM|`ybMMQ^L~5Ma9s2ze*~YScIdl%3dPjlKKk>&*t8 z5k?L85g3UW%7AIuZf|vEr8e5!>29;6$!>nO?sPkRw>w+hdrc>meG=XFc58jha}I>9 ztTjS@XetMl*5jIc&7zAz@J2A4#3}d8+fqkK z6$;c$R{X&U5w`YHpp}XnC6Xj4QbZ#?{vtZPTAQFEfer<^z{|)G8%QuPM>G=c&>TvQ zo76B>hGJrnEl4^+7%qd7(N3##|BEkpMZksk-aY-=MV|Akj5W`4vh(!x)ek>hTUc;B zODG1g?V0eX&rBRI7F%Cl+S=Txk54dmW*Nga(*{Z!Gy@eSlhPxD(o4%HAHZQH!Zlu@ zvIfaq6?__o-1y4XNOSMLDPD^Sfb!d0mo(lPW3^N)=)xh$2!jI%kAw_Fa!B~O#D1o6; zk9&vc&NGEzF6o6+i}DzUMlSs@L49lmi2h)%B9k|;1hCFaJ8o>VN#ToJ|G2b-zO70{ zsl6r}W%?^yDh*z`)nA%4(k5GQQB_zX@;&3Z08lr$A zyHRcaETY{kX5 zGkTP;jYY=cUacT#%YnlA&`4d;+;ka6Zz63`Yh)0bZLsEsH&p|(QZ!~B2cG~UH#^}!W_E(oK-~Qxsx83nD>FNk0n3tYGRRH6FQN7aE4M@TwFg}}3 zrjJ}RlM^`%A+jIJ)TtoUxN?L~ixgY6VHSw}R>h*Y0Hmh9;X9rm8!CiYmmK37skk9r zLS4*V0q|dA(94OD$}41$x=UH~E}i^EdMFanni6yri_Iotpfa-PZD}^d9d2EdlRo=- zLgiBKXu~K)Pb1O+oGu0N5sNW{-WW5v7 zE%uLgwvX+t{@Qm&{`@a=FV1(GV-QIvhx0~dghm0!+pbM4EZ4qxH1ea*YQOtQ=kB9w zyV~G5tR|;xG9PUeO3y{xRt&+0K%gU$0&<{=Z^eW@q+XZPAk-DV{9yxlQj)n)XR>6k z*FmJD1Hq<6dBT_7l0c$h;3@(EQMf=hYMGbI^-Vzq#0@o8*nAJs?UHz_xC9Ro~j*h@XFiFB`O_lP7&5B9x!A}j#_pm zr>2&Pi#(!2zYU6Q4k$+nbwG#RE~O(I7<4BvA0~O}82W(6aL`#N$#;Y@zTtIUxU@DO zZy`9)Q*W=XU;D{V@oTmPur|cu1dJ2dj}At3VN(jhkcP@fDjdF9uVZ?4XFJV&&P!)Ia#co#!t&>8pVuB<^Xp5)MF-L>sVdAJ;k5v~sZ)iAFJ$z?-oi zW1m(4iSllBdX1YKtIr-ROiu6fp{`!fbEJ`St;UN5zqovvBU2d___E27>fuu-U;pL@ zhmId-K-S^3pSTP_;)yZ^hnlXU;44KW6I6ZTz@P=B$GnMc2;wov!;VXr8JK)#4lD+b}DM@ z5#LCyF+DpzyJrq@GiKCHB_`}DZd+SB#Y{30GdN@3=|K=dMs?-+;x+>SY{8zJQj0er zf1`r|k8jen9HB;3d>4)BQkZ#i$w?6u!r)l9@Q)HP0!&VQsxH7F9*M_tG(~*?GqCYA z7l^zSyUYuy8|_d1L54v{5FpYF=^A5%#3iUNR7xr|As6vUaoQ(g8f|3|_5S~9+}oY8 zD0l>G1869hcXGKfHp>&*fkYdgmh%7+;KevlZX^4zJ)F=8%a_y z+^~5Z!bC;Ggv2RoDJhmWDw88#U{eDMQNLvX#4Ogvi=~CzH|Gu=U=p{*A$sf|(tBxT zsn_EGZE8D*L(Q-gLb(KfOb(LJ5CeGtl7~-X0oXrtb#4u0stg%mBKo?K7B*dP=Y)e) z0h4>+T~eS)cOozbqR78*~IyFh61fp^YvoXORz=Rr|YLRxiM6pC;#m1d82_>%S7uJIUzGQ|U+x;lc zQB&+EqHur?D;R2nLu%^Pt>xv%cWytteP?rVv9;Oq^gaxL=4;Sfy!U_?EG#~GeCCZ; z4;(!@QlDt`J^gug@%i(|k5PrwJqEZC4*@U&(Q*+__8wH4ly=K+9@tf!u!yuUo7i!e z2i!c-elW6D``*RMfBm2IzyDru8vf|bvLevk=5W_|r7|Pf&5pg8ovWUoZ+`bo|3AD~ z{dZUEe{gNL5k<6i}~%HrP>!WVKBe zmO!Xa%J;;d;^!k`Wt5OYXhEusI^d7z)@CSCSp&{ z%%bL%rwgoDaRx4DGhyxw*p#gCfmoKK=f$GoBSJ{Sh+$mGYvlYx>e)<68HKFINY&z_ zGlZ|!&mNHTd2%;yY z^A-?Rs071l8EIzlDb9+;s94d~bRuWz77szedJvt4ON10EvsFr0#)5}b!$fjvg{^Kw zY57Dh^E650w;xk^SNX^Yhq>MS=p)`y_}bULF*Y+rAYko<*7NF5e$rWGH7wRB9H=4o z8h%+lz-Q^q*}sEI#tFT?Q%Kq#n|OG}w}YQz8cZf4gqLB#hnG0&)>jepf1Ep>$HRAV zDgNWg?iQ{wG0C)mit$lV*as}(Co68Sq zL0-*3J@$fYst8~hL%~U$vrC?M0gs5!YNRR&z$srzL)hN^JlKaY>>pSL5z6b60L>q@ zc~}aymna6YpyVs|6H4PE43}(jkZ5c=d=l{B31a03J=n7;WRa{nONf+!TOs5_P15wg zG)pR20aDizmP{3U4155M(SeAbv&_l2GP6gbH2KRfDR`EVP@#yz{tNu3*GF!^Dle!a zLzs4L(r17GK88c9N~y9H5~S>+Q~ydh8t|0b)WEkwK>U`PZ~=`GHE$&WqKXf2UD}84 z1fLL~bJ#dRh6SkO5Yw2*mqrkN)K)GLv&cdqbjz=QG6qY~%CGhU3fZtnpsF0#LOICM zvcCWX5Qf4B(;;}+r%;d@g%Wi27e=&G3rZJ`ACZD5La;q%Bj&>!xAq^U6a8v)a)Q0x zg2WN|NHawTJHDD_SN~$sK!B=n`7&^k_TtpYt)8nCB5X-e2Ud zaNL|3dwuQhl`G2&3#Tt$IDY=z^!|g5k^0lMwT?Wdp0ePCHsrziEcHsO^VWr(|M4&NzVpstv`g2MxBtK+^wYhyr1Iil zt=HR7Bb9o8ero5J-Wz=VV&@0fhre@a>fiiq?ZHxmx#;l;&K{vA_T)0VXGnk$>7lbk zE|ss0u&dCA=_K^%UtrZ>`O^Cgz79Tlz(z8)mzZv3n|sq8jETW~0rDSuNdl$!DnNt4 z06?VrGzT{fxW_^JkO04>M0wf0r?93X#V%_~K@lEhK;?RY)wEAlMk00j8N<9UTF0cM}@X1QD=z-!5adF~}=4fMZ0Zk}~qBqV<(kg#%1=M;i!PT%;SK zRud@MZ^QmWEtr3dT2T{*W=LTG`~2Yp&iuylWh{_UOU#-({t-b`Guow7;j@5|y@24K z0x<$;uq(h&UlUV$;7FewR|pu|MACr|eO@9@P0->f9u&w-$Vrn*al7Nn2?qcpN;Dzn;_w|U|QVH4f4L_`O@g*C2(<-(U#(sRN; z=@L@~@;N4_+a>~SReGay)0Njw*WN!r`t@_e_b%+5nD5o;ez5U=s|~E3{wQl8V-q`% z9@hWnKYMoVHXXC(NFB3>Jz5XIhrGh0chE#op}K5`M1G(ND`UY- z=>UTAqSUVm)IGL(*}}pCp@(@bFdN`BEK2afel**pMCgP7=_W%TKwn?nE5n|HRZd z=aEiKa!w%6CN&yO7KK=0L|lwPoe83kz=x_#<=7U6MOA?M5C=L#uXcf@sccoORaO@s zU;Xgor+4lenKC9+PL@!b{KjM=&(hZQcqa~AP=H+UD%n^77J&${C-NdX0}2<)6H+J! zd~6o>En!)~qFwJ&j>gAgeAH2{q1;{>cua*E?1L*U47_P$q@*Yxuh(RB4yh3ny$aqA z;>-mwKo3e4fKd2;nIM4<4md3&3qDF;hcGb3EU-Zeb7M?oa*YL;3@aszup-%-2xu*@ z)nl?cR7OPL1fsqYj*XUpfk>zVhrEVgN&z3~X7N@7WqM84QkJ<9LP}^irVGF!GH;&tRFw8$DMWoW7s}LN z(7+c^_=|GlO5t6K4**}CfjdUHwv7-wHnn&Eh|3F z3)ny}W&luoL^c5sX`2t7a_g7i6uJnFDHfw%T~Fs zKK=+xJap<5uX9o$cINF1QaDkP+H8sR&=0Oa>hU4OL^3IX88ARcGep65Z>!hxwf4)a ztP-xTt#LwpYpcaxTRoX>ztcm=0U-9wk-p0ssLc@zaSU7UoqYZE1M_o@>6uQK^ zzIS?ZcF)vglNk;6GJ2j;rMkVow#jQ;JzGgvia{VzWubJ7Nfo=%rNp;uxuI6N3>UEn zD2#7F3nIk;H9~=EDM?J1R4(*>#%OG#U=CKz$+~Zw#};%c!O~{x$?6#XFtr$%HU#{D zfI^OS<;2f>pI`mzlgsS$;bAa7SO6XvtCSR!jFgz_W3aQjxNzmi%TFIXI{Vg}hmM_S zZ*Fk*A9ACHoeDfH6RP^ORPB00MSjt?+bW={&(nm112e7v{Lc)2 z;bMPuo8cuE!tP>PTdHa3Qm2g37CnRpYXVy1y0&qkS^wpC1|PiA{r(&EfAZt)-~G6{ zu{1R{!)TQAE0AtxwbNI`)QjMyQM*C-Do7NGOZFGU0?m>uwHTahK~XHEs=U;Z1?zC9 zfrmMgoDEU6p>{X8x+zi#C<5m;0h5BnbuQrzH2%S2G-Z!A%ePUb64R;7mVVFmq)Lz| za_QF{F#uBS+=nzUIOE`Q7JAgiXzR66%9j-eL5MHmUJ;}PlOiq+%*dK{+>x*^^H3Pk zFx@2$GYPg#%!LBNBmWvMcqD{jt3?dBCOfOEx`@$K;6j92;Kzbv85RjJ5oA0O zJBVvwyh=-lwUh~QV~cW>Rg@yXTA=g)}vjfp0;)fUAdtJmi!7&?H!S*i>f3{36$^62?jPQLm&&lLF@ zY#tuKV2lGcm70RrxFaACCriRrh6<3AA+DXPgbv6sLMlruxrQOR@jv}q;#CJfTtnY~l{W;sHT6U3QXs4!@Enhvil{@X_EY?VdfFOB5$h9bDO`Gl(Ke8+oIGt-GWiu$z$#d`w1A5o z`Jj-k4iCd|nG=7MzpUwo#fV}CLF$~pEeg=!fm{IqD7IwKH~2`R#PJWG1p)Xe2<`|V zm~BU>h#Q7?D1vU_V4biIMBUX>20#Sc`L#@mHvqC;=(FoxZe&)+D?Ybt+a!<-1PDjo zDnl74TXCnER~S^40(A>UY0)pN2z*rJ#0pi>09Yvj2=O7MbhXK%M)7p5jeCNWEf81) zZY8A^{X`#vpxYEE)CN_S#)I8vk2K4Rh|wB6*y8i2?D@!qV6CWRCDauDEQJ+>U6JU` zrG(*zs~CV>(}@#W03#6%5__f$LL#Ktk|-mwrw*V-Ym1K_a1hs_2M^|t9G==U%llTC zm0^yS){td(C?>`U905ZFP+7WTPCmyVS9rq~L;m$OjttsZT4Ar}*7`HG zW=AP&0RTU8{^Xh0Uq@X82}0_^y<1;h{-m|O&Xfk^LNjX|$O*9a?-^l;Lgyzj&ZTrn zQ@APza-@?Arjw(3u_8HtSrENhGH}(0EHLloi%EPjP?O_R_8-`H=-|G6^E3PBI7^XP z4IFW&)pG0caKN$0t@Vxe>gt1Ax7U}KwTM_`bOurgr{e)uuQ0@5LyA(P-BMVO2@enq zdg+HKHIxFx3_HPX;|n5@f3n!? zv9Y}L<)zDau3YVOw#Rsr8r=|pDLa(ytR)0#k#fL2kDu22?e60nH&&iMJ9O-LZ>O!e zVN7xzW)wlA7C;yxLS436rqgP>O{#ngS8>>@W&>&bk5_Z*O5c8S^cP?4jdJFFixG{4 zgCgCxct^}D#)b{(4!n;DG533o0XxQP!+jIoU;gIId#6Xw9ohPO|L<^dtzMfMt*{-) zA+OT1&j|^V=$C=itKATvV-vYIF5AOJLF}w@@JUsJi#>_7ILgt!oE=>fgrz?aP07SD z$;1EvKmbWZK~ylBsF#42U+xeiiNzk}-L|e|VA?{$9ORTl&6es4Nyy}g-8=;t(LrA9 zEL;SR_azBbnxdXU%Z;&_F5H01L^BG7BL|g~ba4gTp&M(HxR8mw48g?)4VS#TL#P&I z%t<(kHUaIJA0>#`Af-)$*7;06#zCph$|CSW7Z|i|wcwu@v<@QV2E&LEF+C>b==kOA zzV`$(Ui}A+l#x6)r^*Oo&s4Mrt$|&CKpJ{^Z8{H5L1w?&s61Nk|Hf}s z{;!{Hk4#M08cdpc0a7=dl#dj^&vXz13#NlPkaa_|o#ivvcphd-U8Z zjWLdQb9{g)ipwZdu;VE3rAsi?9Yh&q!l=P`{ zd+G|-F0trww|ZJBISYUYklfYBIb447$6j6H@?l!sJeM$Q)qE$TE} zOPINQ!KuwcB50a);B%`Ll9fV0cdnUA#uGAt+JlEaTN^bKRjz3`^#>I;5dJ9+!Yy;7 zUcxE9o_1FVk%!gDBgp7Gm@!@nMZ_j6Vj<2@L~t=iIuy=jM>0iMh0X#EI>gA(7VYUf zhSNv}dQ1whMn&X>L6yD>G6a=jNe0y@Y-shAiL#&ws6ZkM{Q<^2iO@atKC;!PER}1e z#ywmJg>;0M$?fz$0ET@bE?GuGkR}GeQ61^&>6zgIij)J5fn97%>2TJTi~^W$ZwiPR z2o&U-l-!0^NQRk+8fuD0ka^ zQWpgRhhZ}P6H3Uq#2A;+0uv7PIf-uyK+u5M+hfwbiE& z9v(Pyn5RtHNJKE_mAz+Pd!5;lyVtLERyKGlkoJ3O|ADh_yumBRw!1BM2rWNb_~Ozh z8;eVFV*J4T!MS5cSgoEtILG6Ugf09U;z7kG*!qkw5W=-*_Rh1+(%D#FTV3O= zWK304UAPhlbOa-1&_s}cPOc?UUX-hut{T}3ONL6+)X7Gv>Rnq`#iSWG>Bj@W0u1Kp zkd+B#rEVCd2KgE=UtHU35}=p-7GQdd6Dp(a&CP2+`|!@yE39TT$9TG$%`GqpefFb( zrME*&^o4AP`UP_9~8M2GWR zFJNTlhNI!>gOvjl?O}(xeW2~o#_!2e^x#+MuY>~fl zV*l9R__gNo1C`(St?ft46BASPRxyn-2+(3c89$XcNm8(g4fC}XwV$KXWCc__5J6n$ z(jC*v24SZqhat$JU?hfC)GtYZrifd6U>o6w-#8vXjO?Hht;|!B+Wu&tpqCWIA4p`H zk9&IlfNY{`DNx6j2=3^;NL=#ZRKl&fySB=G=(i@1Y&c021rwUFKQoQ~zWsy;U{Xx$D4HP| z34#!Dlc67#0V(NXhAZVGXoP|Fb4eQ1K@K3HS|LG(`~XN%Nh`)^m&wAgFK(lO#L8BB z7hp>1GPDUW@}(n;@>^Kmt(9aT7o8O0>}peH%GlWHJFucwKodrR;0_@$g&THOcee>J zgXZYYWW9Cf=-_*=j(+#e+B;{f2luqbtkkb;GUdSV3-#l04j{rAAUn{FfPep!(SQ8M zV_W^+zI|*3MO`4kkY$@O02q-Mw3CSC=%u-_QI$ygwhDdsOIhgd)Sx1q;x`(3tdK+= zAfdzd{NWRbrngjY`e?Y^03vmuE{4x9_3gM|+H4r%>}GuV+5Jb`8@zjA znd30dGu`gS#`5CQ^T&@CA3fY$ULLaC;Rx9Ml1g7o3~)6GSFjFBq|BFEQR$RWASy1G z*mAB@P%+W6K)f=hAmITpz{X&B0;;~bvU>CL&-Uz_o0^}a-r-ZH*WO+E@~0p5c|Op` z-1>|Y4zgePtv5I!0HZ-zJa67(1!HX$rB%n7o?{OW4y~vp%n#R~vGIjzTG(gwR zHXXTB5n+up8HlC;m=T>cfEK4MZm85FAJR111TV6Zt6*$b&|w^2t0)^-xH4v77eY$0 z+XE9wAY=l53_G|oL*dj)rsUCpHC(~A-BTYrUbLr`8RgKKkB}ocwG_(mCJ5B% zEE&po&CmpW*6CXFE@g(&?#}w+ z(z;@Gpe|GmN1lr*k`b1%S*YO-+N8rF2DAFC>WwpVq4_FpC9G?Uo6=yC9rY?7iI?<$ z!ltDml90(B!sm|{)}Al#IeL@_aameryVlg+Jr}?E?LGSs-2ME@)4TU;jpi$FUOasA zWP4|ag$Q0Ecv1cFg5q>!jhXYcGg7h?PwTV|g?m$h% z$fWgPV|{sZeN|wHHsa8(`O~?|41j7?(~&;3laG%HLXB8d7xpJG6mD7tXYr}3mgGNf zKrLBv+LIc}hVY^v_(D?E3{|YWZ>+BD*+0+P3~zow@xXN$qLQ(XI;*UAu3x`%?P|Z< zZ#J8BP-1bh$s#Mz7|z5Y+R#U|^I01vm{?NcgUrDVcJOM#5|2Tqex`Ezl+`RtgDlkAw*KC7!7CwwE2jhUGR)IIIPk~ zk=_0^=yyCOn`b2IJh;Lm=tVuSBh^|Vb>-5afXxLJ1+{|U$t-m#B-PTzx4gvyO`tG^&OZR5X_lCc!-zImmL?)O#!Hn zM9dgo5(-vQ#LMs{D|u9U=gI(x#3Oi$Ky)%T&rxojJSJN00A(@%d}t{1)ee@I2S) zS6^?hZrr+Zh2VDR*vW&ZPaZyfX7Aj5uj{KYc!>jX?#cZJOOKzjB*m-r*!dN7%3V^R z6(fWQn;`fVzY1Ym(tu$SWYKxx&U7e&vcaIWy4qS@rY<3tDHKf!5O4`RJwk@Pm0_Zc zQ&+s+#ICgZ)flxazG?HQEC6y6b;W{qASHt#Qc(eANl~SiL%`Bc8s(T&(sD==psXM- zkyGU5e=iD)&Q(xKTJ3{#;Uy3mC{hXUW&8)zSQUWKCEgcsD5aElvIVY!Px$~>7*Nd& zzQLOmG@s6#`1xo`F-IwvYWtYZ=Tp^)ZxDmWunaY<1BP79Ldp{1up>0;nkY3uR}87( zk0z6-KG9Ve0=}ENZ505gZG!gabO09UAj97|SCP#~T#wY}-;y7NC0pQB3{e5HKqe1VLQ!fV zjP0)wKm~;$ZgUld`BdkYEMZFx&^4k|GG$gKIK!XFNXu8~PrT6K#0xPmC7~ZO2;j^R4s*eq zawZQ)YBHD4en3L7Nfh84!}0((u;geSUh>N=kpmmkLDVDApu>;B6ltgt|3gbe9ODYN zus~S|lJ+n{9PV^R$42+?B)~+I@Ci^l+x2>V?|}n*=jVC4ckhwIXD?o49o5Z0)!OYZ zuHU)xX@7?x>#;x4R}n6x#LbK^QBlk#obL)bazw0(3HnuvLP-bDG1C>4QqmNPE^I7J*nk|+;|u3wap4zQiMS3FuK#- zkog~ceeA$oZO~>PbhXxO^lIZf)sdz3{=!CWqgCJH)vlRs^5`p$pm3XztpeneLGQZ5 zv+le!c#`4S$nSqTa``@u8JUil;4Z|*9p%??Ig^YXW?1d+yM>KIN_xFUr^B$JGB?$q zo#27@@!o*OQ6hC)IvjEmX7u9R$e1GXKOcRBagA8CrEHgrM1(BfSPA_>z{?xOX&U$=%R-V-+G^IrEFZP{c3!7R6n_wS%@ul& zB2+6N_$XUq5V@2O*<34U1+JDgfFaC7zSKjM>{mkIV>|mTd{T*f{uwt|d9M-{TcXvdjcrjW)^9&WI ztYlK6LBtw&#IQCaiP`huvLEXIU7!!latfa(~z1D z2Qs2ZUEyueJi>7Evrix0xS=x3BtE?vqGl?IL?*BNNas(P|eWAf5hX zJjw(v5sg+RO=?X!L2B$j=!mk$JXs+Twgg|LNnj`Bp`*vx^xx~bGuD0$D&a}kF`Cu} z=Gte@h{~S9LC zL{Z&aGlCS@mu+<;llqaSDy6y&v6ttOdG@Z=otfXacW$oB({#%;YDQ8CnAKWiywTZe-TL%X_TM^x%ClK7=~I0EAt|uff+;a*Q3(Xr^vC2XeO7sp z3;QSh&}z|mx{mDi-<* zg<-!Kp*hME=>Bo1of=!oQMygG8HI8GT=$~f-g)o*TRE7qdZhXBmoWrWg?X|0MmhBB0zyUGPdu{a$gFO zY;l7?=;6)?2GP)Hmni=Pp*(?NxOoW=QbR%!f>2;f02KF{;?@*(G7AIf8gN!!9$}H4JLtj=`WkhuYLIA?H9|VbzfkP ze6fAuY6E=04Bw6-Fb!#mNJ5r+NA7m$WG);_PU=T7N=UgDLUAoTm?RFg@6Mk%`sTO4 zbM(wvwsUrOINrMJ9uq8KSaDWlj25~7?B3n`w{H^hd9s2D)E%at*qhkxJ-T`0*@H(u z>{WCf#i<|!BL};71hwiCiX;HOJX$a<64EkReMa3fs-WS9d&dU=ML@d0w8KWvbkHaT z=jjGAG3AVa4u`Hyw}wI*4mMXe5d6f{^wjhW<3Q|y83D@BMDcvp^^ZP$cJEHTKJK^{ zSYS4qM4X`0*V3#-UJ?_fMfdX^3a7sir3S2UfKLMZ?XIcbqJ|8^^ppqYN0=TpbGjq8RqYK0qEKd!JYF`46E=3f3 zfEZ~FcQ1jb&7_W%uvNZgNovxb=Q<`4lNJ*>kOCQEfR_kD2vQXR?1G?f>E@AHK!BSv zqHtrz&s!Hd$Z{?~DCOEcSfL@@mW()&wJl|33_Id2X}}~o`T>D#A`?Ppk%VO1n?qKT z=ub#dxWzVNc|P38#)iZ5tv~nP@K?Uo|FsYL|JnB|fB8=ifBF5v*G_cj>fJinF+Yw) zVYJq#$HY9pkKOghYHUu-v%RhZN43nCgOZ1KAd7$X8Ju8H{n6+3R(F)wJIIMy_8cTf z`%w!TTCv44xvBPXJ1w#B8^K2i+{d!4FF!P*;-tE40#_XZReRuG$)*2fm_frx8jR_z zQa&mHNGWPaU>b@@yY}ok`zOc8$ENr0Wp>K;s!|){vHhLyy=zx*d~%udXX#c;Ts$ne zqnc(>PPx0Owvx#8*-(Lp0E)P zJoXn+VZX~BpMCrHv2IKf#puW~vfEf)CxCeR&G?uSBH-Fi@I)D_JhiFWJ%>)5;6)s4 zZ|CSF`q$C{PDV9glg5`?NHUpBFDqM!r4-_)0ALk5RBLWXH(HZD6bCzGp_TM^NRk7_ z8$6hM@aQo+%7ek!fF1|}fVl1e1K7x@BU zYMTw!Y(1l_=BJ|?Q;YGcT<2qpfUKpzLlzRW9zWs}3z?2%baRTSSgBNq6f*9>o%eyE zD5U^2awXm{$F~8qCrH;;3oXZRw6;z3W-X?E~Y}JAQTqtX|ak5s4BrFH%4rzrWi57aY>vN zfv6?L1-RYLkFuaQf}1LP`n)=@_9GkNg#wY4=9&liDHd@6ZFdZYMd>`0_noMri@}SE zF3M%wWh{~@G5Al@Z6FJ5r3@*)<|k4`pj1PQVi5o( zxi4~t9LuXUBqT9N!O=Y_AS9a;Ncg5az+_D%u6;1DWkx`Z&^f}cZnp6?cpvAr4?kiq z{oEUGjZ94taoBmnVko0$VgWO_Mo8Mb;qdwW``gQFh~1FNG`ng7sY;VnG42u(tm;Sq zR(GznK^d_-Elwm@U0yu%&bxaL9vp3q^GE{|7Mm^Jzr4tX5$2*H#gMN$`O?}^ z7H%G0!bwPuNg_iTQ_;2>iqj}A3O8P`1rUUSgrC#mD`s-etPQ`$N#em1U{;26fsb6VQ+d*RI3F2 zC#l*GT^J{|O<~bwxB1~IHoXFdqy_ogJN6b2Kq;b^arN9O9& zW35yBM*sAM?zdm99Nt%}?wh`Gr~beG%g#UfaeckZ^grFH;#n$Qdsh5lXZXgrsPX5% zHS*v88e@mGKfF4;^{C$EeY9h}>9N``ou7X18~xU3<&Q7*Ke;++_gM#G!!~-z+>fdO z28`&38kt(a^$17qd%$;GAXa)YMR04aEZCr1d8YuiHA9Sr!X;+7YLMz$tPM^wr}&Cu(F&+&+*3ko zP&$pLfuUmI(3IW(UdtYu&O{{@FhXX<3@2Oyi+cqEcd=vO`z(tzLl($lAW$D301p-N zV&fM^EobbZEO_AzgMuDu1G!EiO%lvUdL)PN1&NeaLQ#JnnzN{ro3`1!B2e42BnF&T zRw@)ZXQ!D#)sRJLHoUTF8Pis$%E`be$^2=&6c%;Nn%G5cgr@K`4>C$|T(189>B0Z> zS8B(P^efA-1h{^sWi&)mgDNMf(>`=moZGZ{S^8FOa%*#I;(z~F^=@zSzx=bqL$jS~ zuWN&s<_}q@;qHJ@@Grk#{hiCr-@Mds^jYR*vpo+@(k39V*e7gzS{)=QG~@+t*;Up` zE=<8d+7={~Nlbo|iAt3v8=KtvOD*<@C~-hp!e)#bYK+jW24Oq029-Ksn-UI!oGqk} zpwy8@ZJdy@bobt;o%XAp?&%A!j!#X{=h$9by>sQt^~)b~{>13WxK%=>qo1IRg%O)Y z=d~KEj z@mJ5SJbV7t%4J@6<=_EgfUT{@Wwpy_#(6C1RkV=B7d{hRmA-|-mT(;m9suQ`EO0>* zX+SB=5Py0*3%70`J$?G{iIZFO_St94Ijx+x+~&o9)P-*z&>Q20n>RlF^wuYrI0nRR z1Hcf6$bm_-LZ?LWN)+Vq3J-PY9g4;jHKbsTCn#yjg$EzW^iezs)2~$#RSZ*6jrTs- zNDl|95DDNw9VH1vi&f1iOT>r-ujbB%1$GS+2?gXBr))6QuZT25`%5{N)+iN&P@^+>8fm?|W&o5HC|VFA216G$(!&C?4I)BGz==D?sUzlMIaFo82mAn4 z1WA?dh!t;C;7AtsZ6L&n0*TxBL-?nX6kK5)$(K-DIqS9daJ$2Td<0o685|0+BEgf zpDmx30vNEk0nL&sRX!qv^zkffEuxGC@~44<4OwU-+NgNpPwx?&)E87D(Eo%{%pdi! zDJ4ctL1auWL;x`?$QJ1_Ul3|s+*(EmFrsFmHFZU+3Au|ZbOMdK7BC9Pi~k`|9g!eo zzPm-O(7|V}ePeO)s}C=caQu}s92M-QBn~3*fQN`nr(kupz4Bs-HxRQADO9K~nFpZK zK`35%z(uCRVI-DIA$G5+4>DyUV=PpO>~tY63~2nSAUM(xB0NqfCiVEa^G8md)#3*| zI{m&3PVuPfTC5RF~VSo7*zq|4KE^RNrrFbs<~NN^*Lv+E2}QU1P!vIWOc1H zDNT2e=`>a?79T#?;%R^<&kmh9Rv&Njc=zKwcOTuk)$jHw5T&a->X(scu_0ioQk5F9 zOH4HE3)`SUZn>4V);>tF)|R> zpRIB};6%4w`P;wQ`3HZ%fwZ;S1ScEviW-VD651J#j@QPj+dG4;`@@GfJNpmxkG<1- z<=FPo1NClo{D&WQ|F?g>^9P@eb{iao9a)FEmx|L?l^!op;-HI>{+~EsIeny8YgfL@9Gwg z_)tjSmTFLIVsttP1fZjr#|oGyoDgoH4N=;oi_geDT`DRm={%up$58SSt)O9=QB%L} z{#X!#2!X*G|G}xr0}Zo~eK;38s_{r9Ns?mGQJ!0%jr{D(>finC;a~s3_>tM|%GOq= z)o{38Wt$|Yeel9u49B4qXsPELhpMAj9?$&U-|NtY{Lb68U;0{S*lYC%W4!W=$5W6B zBPbqm8MJHrW-5R2yIViLHGHx*Iy=h&Jajyv34H+yzUgcuv&cx9SQQLfmtzB)Arc*3 z#^7X8+cC-$QlJEC_$7^^>OgCOSbjpO9+1JD(>inl%)$}KrdD*sEwv*<^ao9LUg(St z)}B89{EvRT_+;VW(Ze{*<9oNBe08hS>3U3@ah2MN9cGF(Q&aZ^9Q;HHqT(JqLB`%- z9JKo)k-sPcg6t>Al9p#^N`2XI52fzFhp51DSMVr4D#jlmfEzfa_q^lF?7K`)*_vQc zJX+Z8zj*j~?a31!$XI>+Y-!;c9q#?dj?V1c&*BEJIOqI50t=H(6XO%J`{zb^?20$8 z@Z6GJSa~RJfN;2jOK4MdL9OUWS21KDWeuHnQ~jYZU5k5kWcwqd3aV6=aZdPHf3bZ3 z`j<0oxE-5t0RyaRhi1+v(pfp(URk+y<+Hn=f8O5Q;#Gjyiv0x22;QsGd&5Q4ga1`T zgi2c8`=KnL0L|leQW6wspHe`GnOOo3bTvTW`nrcs&!0^2A6=!%$1rk5j+_RXtVxfY zQx;~8XELs)V!1Yx=n1Tm0gak)tj<5sC2WX@VoFPj_zpK2Rhq7*fug*vrC_Nc7^y`8 zYD}ZDRLW=c0$94G6a|8FD%LG`0R$cbpLM7T2*2e_K#G~m@NBA0hUk=k7@{5t#9;;C zcr@^ltui1JSz^F;VeA|bGYWC^H~}I$`jWfb=bS(W7*a*e!i@x^mO4&2+}@ zW%RdPI+W_eNZ@nd@(UazLjsB*G83fm48Jlde@F_d^eM7c)vgI@0%Y<{9)O`sh<>RD zegR2!RSdIWR>GFE(u z6jJ0A!dwtY2nDL%2qB?8RKo@!EH5r&p1AQjk|CT~tDoD1*kPW^p1r0@pk99zgCi@~ z2u-JUp;frLB`6^vT31E}qfh}JNe8a7fp@;i1~{eh0!f1<9yDMbV0GpCPk-|C?rjbU zo}AvZk5$ye2SDbjgy5)-a4yZp>at*%SP`Q_*XOjQ!lGO%7N%jLh7Seg7Rn=Ab!%}7 zA$zGzIFUB>1|z$0AP_4D_3VN9JX?JDaB*Q__1PjvB1oYx zJsh($)S!#2GUORq*i*z^J$^>}1_xL?@+1~%nkkfw0ZN8l528D601UVwCd!uoux>2| zIj4T;*MO} z2_H~oA66Y`Yij=I!l9)x5a5mAg^)|w-vHOXMOM)jgYp+3V>WE`ba#H(?als zrGZSvWAHIJH49-Lw#bDnh^TZ0f#R@6rD(YJi726b!w!hZ;93|dX#9vFCh~D6PeX>IQponIPhXd)lLC}sG)|DC*;M(0$BUf0jqd1J zDQcFKpwDyl-Gam73G%UtG4OkcGO}i%V1JFdEqah)Fg_+Y7Y7sv$U?93QAo(0KNvt9 zzP6iZQbwMvH2&6a?p(Vw`0swT@r^Ts=9n)*AB=L~!C0le#io83%P;_<`&C}uS{?o3 zN#n)FusSjR`GeN?Uad}|2zvYk5LOI^Jbp@tiZ!uny*K#Y8-qXfR_h=Aes!liQ6H-t z{?()63}yUADo9J+AS?4!y8fRT#TWtX%W=^K)~bR?QTi}O6~e==2Goh+Mi#YhnFYGG ztIgSTSWkPwU(^~ncgK}nGw2#?Fl^miUA@PS|F6D6o!zZtX<@p)&Mv9}%V~1oBblq?LiO zVi#t>`A)sgp!ekN{X-{D9Y248N2W1jl+5xkpYBfQ#glt?uUvh4=QiULdjABhtnMP+ zqGk1rf#D5CO+XWR2m?$7kd9&oI1A$i6b&;vkb^&G$Rl1IUFys(B%wuFjyV_&jVsP| zY848=cp*mglqd^Sa&6~8FDyw&NTUWdLy$EhLM$VaqFCT48gUmc%oBEMf*LBdO(Pv} z2zAm0od`XqCR7b$cY@7uUg$F;w2+^kt$1TuD$BTR0n)HiU%n!D;xZUSh}Mh071}gu zNC*V2SSyty)64+4(B#)uJ!aFTESgTT-615J*vop03Jw}LqGI7xnqkp%Jam0M7Q&*G z(iEsNgQvyGk-ZvRj*&96BOvp@Wl&Py zN)%0qA)L#M?1^2jF(d_6d{ff~JP^!GJ|s{v=(z|E#H6UKjr<_96qICBDGQDKK{S9! zlI{pzpyrw_7ezSE;n*QkS3w#`_SZs(3|d}U^Hb*804pCav2#m8q(c!gNK@f}D=8t3 z3q%##Vt3A>QY%ezW+spV1cMNU6}{4d$7nr3Dc~no2Lh1@&bA!FjD6@Z)}v76Tp}%! z8Cyw=N0JC3RHG~yDV9a1n1jVr4_f>M0$y8eMB!;vT8*v%Ek&S0Q97;z)JC>9HhHmt z0lQK=^vc<9{^0w~nOP40lKt9nb#ajmD)gvj5+Z~aS{0Ib{MPtomB0xiF!_gVQNzHZ zSVTx6>@}*g7?hKa2q+_Ks*n|3-b!}nwb$kk9>j6z)c?c0k!Ax_<6mOYll}|}2x>OIlePok&RSx-xEX8Q6yuprQS>ZMw1PEh> z(5xt>Scwrtl1WD5s!a*1DQep23I?w#zyxg_6+ySK>Tad2<&`bG$+*vP{>n)c5{adN zX|x4ZiCpA~mSXz6@XD9!=28<>38*BJfV3j@!U82UUaHgkF`_yb8c^0Lc86nQlRCMu z0^J@uJdc|J6TL|CMj- z9NY`|eav}(c%}0j|EzcE#^lIe_Vm+n;efgDfv&)%k;IS~p;arLo#AA?`yYQ}PcXoJfX4K;$W$ai)q!tH* zjnxRl$B_;r)B0eXnR@o3+XGFk6tAH&eI5Oc1X(s;M@zqeU?r{mG2_Dsnzp+#J-WTw z_!mEIvFqo*{qwcg&#-}K=gH$8dXES9j?L6sZXdG>0$|+YUIWM|w^f;&sr|)okNo?qwFgT(JR&dF0h&R?=!oKWHHTiFI#H2?A7hJZ)Bw<(*^(CxArgy8L$aB$ zpd^CiwyesGajGWny1fP0JTc1lL~x)2>R}a>%PkTiu9ip$&N3^2Gh?9{TBe{TLUD9x zhe7k;7C0gkWeHudM%l)MW#ogII4MMVXJUB~ z7o76a+{OF%mmfUffd;0!UjO=g^CwO*&f&EC$$k6wojkJj19b+@q!h?HqE4yq`rloXU-d{ei`3aObd9sEI> z;JhsSP^!REv6=e>cH~AedkQkU8-Aq^>a;s*t9qmXvhoFiVH1Lb!12AI6euu>5gIvm zSwea|#X&Sdtx}pzb3`NZDWtf(u&_y{d5wg);u}cm#gLVIiBuNkODrWoth|svcVQ=D z#1kVh8)dJk0|Eqv?*comg#{A8VslJ$*ox@16qgwIlznRjNWdUHh0iqwNoEPs&Wcrp zCq)*IvZ)WHX)wqu6vf1CcQ6mzVk&TB^HmJa95^5eZcHUV?Nq1-gZ2((aO2bkH2?cMfd~;SJ+f-$ z@Y!=GFJ2rOAG4nr6~oPo#pf@cE--PzPH-S*pC?>qm#(fSJBUo}r(I!90B^;n(18rQ zLWgj5$T*6&plN~^c|ti{r36&LiW=i8%u*#wTv|DdLo3)~9cV+K7InT#lTFR?@}+H4s_kO4_>Y95)ukr^$s{8B)A{qq5_E>miYD?8QtXmf({9A9A; z4%n-XcenbLt=^&8&0qaq{lEH;dgo3LIU;?qT(3;_?mgfBpMPuQ)k8k` z-CDzb4vz38dU^Pjq%%#KBUmdtJG|_?&s2G%GMt_o9ywAywtsMFwzp?|$ODiKrsZq3 zrFQe~^TD;d{b$S7Hb*Dc8&i{Thgu;dqSCySBQ!u1zseH1C|)Fr{o7gL6Jkf1IYl45 z7jdgq|5um0H(rdrevHo8@b;6HA__(J8VLOU^E zg5gGRa>u23q1Vy5XHgPqDFB`LA6VlBKJe?BAhU&FwTO?;bQ`*9>w7S zKIyC4GvNvR`k32TyRCMwwT;#B4lJs$5eN~p`hDTOZ_Li_eR$*M+KVOkH7bM{Ph3jA zY|PL-0Y@K^Ox*}NuqetC3yWW2*_M+)1x1Tz=hdxx3Q<~hzSQ1)@nU;(b9&#rJnO=h z#mA3tT)xz6wHwW8gbxA-h}7U3o*k=~{-N?$07_253RaV+B!8h_IU{i5Ob`MAeL#ZC zlhk8mL#}-B6%!1_U=pDr!?7osRMbl$8oo#nEFA0tZvj5sYMrCxU`Zrg_WZ{#xm2R?w zEEi9N;?OgbSfGSumF_e}F{@o1_%AT1gVjir?j9S9vM8G*1&%r3P?9HI@Js|H*>0es z33G-)>B;yCF_Or976;_#vKE@i+ zS^>zC1$kKuFz4Hf}lTG$?fS)to>8|lu z5*Um$bV!L^0I)5|h(OO)mhcGJm}f#bg=NT=O=ICsv;aY&5Nre#rj+X2OtwT)to1V! zGk5&LE928MZs>%*mNm-IMJ$VR52<8omyH!!c(YMcM|EJeFhj+9q0${Gjbt?JW{B@bf0!W zjcjiKs7glQ)&hhksJH+POmEPd+OrywmOx2y6&~yNmttu9WmrI3&VgzZ8Vv9u31U)C ztU;te(5bRTJE0i+h`inPBCQP0qzg9W1qEn$5XSads!5{e8v{;oM_y4+y|KffrEXK- z=0)4ce6%s3Be%1$P51V_3)R2=gUYXe(3+X7Rn}YWta0w0RJCRZyu~!de`^8`*!!fz3+QZ&pw)w zG%J!25&}&?0xv>|jY%aoe;B)*qEb$!Qcxr$f25L%V=84_I3*hh1cPM+MnZr#B($L! z&1m+$XQronx~K2GeV2D%^7(wv9dd8?eb4)x=lLz)-|{TyIp;7U821J|5%9!;>g~rn z7miMs7N*1gePS!1r2pY{E#oE zNs>#h3=5WI4KLt>GN%xRuik$ZiS-n!6uC6+Vzr`cgd7z3F?%Yn`I2=8I#&F(*PV=Z zCW{MDOpyn-A_zR6R9 zQ5)>I5YV14_q+AyFHfI;gHF0O?#(YR5B||dc|(&KmA)M_pn}af9&4!2QSoXldg}zhmpw`P6qX)JDFQ9efkmJ3pV@y%hPfbH)li&-5c znk%)1IZ~f1Y=epHe@$nKpd7ixXdoG*_VoA>QG`f%QXD?3i8`uY2`OD9$r$f0Tsn0< znH1(Izc4rD4xGTrT&)o18G8UVLoTO5x&uxKEyhCeU8;JMP2snC7XBBDh?>%SR?l*D z01=wee^ZXoaC9=0J`P|^iSRMsSrggK0EP}0cm5c=JyPJ^o9J-{$5qYc+jF$W)oWyD$;w1i^%4U34fAoE{~?hbV1Q+N~kR4=79Eo|Vof)jg#z0M;~qsTsA z$tciB(9?B+F0P^&2~A++7|Ky58Ungbs58*qxTYUbf%q!lVWuozR4ARpurIsI$M^Oel9P2zhw3__q|v$QwK8ww55;QEKz|k&Sp9k zh=74uaxytYc&_2o&_k%F2Fx^;?rUHrNl2DM`g|9a-nA=&P|H4)mJmi*4wHQ(k2g5T zgNo28Z5+X6YE7(z(kMyIFZd=AuZOBj#^Yd(?&?#RSkYoYL6j~(#S)RC>^h2&@?T@C z2^j|249Xa#7)BSl8KJ1Fi|a#LRGM(}P~qr?G?Qh@Agu9SW%5U7Wc-L1fho}jFzJ0W z?TfRBqCtW37*~`vhgjO%rj#m@)?3V1Xpx0F3MdI%yAb7%F?2!1r8lBYEMarE2PN@{ zgQa}xMuezY)fUvs*a71>rxqJzJA~Avh6U`Ix4mx))UZAkwC};j8oZLst0?N|r zelGQLNmRpdfqVPr_I4Mwv?eq;AloYimWPCa5m(XSFd3}CR1*lTi$t8|n=z(Vj=!}8 z3#?AVI3*Ei(%&-uWNGrik;8{i9%J8k-v@5TX?;!0gqI_7<&GP@uD|{oZ^`v4tu$#J zj*fN}N$zEy7t><u z041rzU?4q6>ZFo?;!WmjZr3C?=mffCfT5Ar^GCOVjAK)|VH{l#lPEHN<*X_g2l1i- z7E^5mBMaDx$~pB&5lU3pM+MN1Um_4A6lm)+^ig^){dPgs?CjcI(VB7vRUnkZ-oXe+ zva-nztuag$l#uMPzoj=gGS~mx4_E&Fhbp(9LW0J0Ylp2tt)0OAsoJ0M z7!eRN^l(*u0{>NVHr%2HwE0R|jh74@-CzFj9b?wzeIFB#T6d799oSD@?bNrrmCt=^ z^5*qQy#ww=KZ>x)24JGI;Y|)EkQe=&E$6z`csQH(W}SNJ&eN?&Zm)mn_R&Kpw&oX9 zWIU~L>%uG7OaJ5VRX+F4{%p!pJ(5bxS%lbXJRr#}yyDM~rqgzykCnZ)2Z{s@2$L#P*QaJunx?nm#V0qo52WF(s8aG{4Y- zRN_vkcphj*1R6;MPuR7Y1v&wn2M~qLaLnb|ufjGeZqV=qE!~=}M$DR7@Tu4NH^K4n zCL9B_HH9IAfuxF(dMswL4dUrMN`x$Y%FB~UB9iqy_0_xqW-Gt}tm-^#cb#(6V z?9)HO4(e*H_;FMoU5-K_AeU=k$aL}W};}YNjYP`=2J*mr+63SbHmT#lOcRKCY-W>nG zUme|Zyt;=gK3%$WvNU$le%oJ95Nz%T(2j~}GE=br2LMoKJV}zlxC-fL~#4~g|ZRd?~u(J|; zQC_=3kldzK?bCRfK{AkB5G_euK01+@DW1a-M9?ib5L#*nW9k*YBi~)&1Ri+{1lj4F^og!GDAZ68uz4BBUiX*xY*v-Q# z5^_$+Gfbk{TTci~51qMi=L7dIuI?N7xG|#51nk?>@Uu>9Vb3a83WjcHq*dlE^5!h! z#Zr*qsK60Kl_+QI({&^j2aHTrz*6N?D$vO6Y9u;2KOn^q2ZNjI>xZe}#iB9YJ}(DA zhMlb~9!aFXrVrDK{MSd!4?<30?S`W!VaY)s$+!Z*0U4D!!GCF+VZmJk+a+=;pjpF+ zr7}%4GX>U{ZTb=FdS1~J{ctBv7%9*mqi~vbXhRhm>IQnL0D+x-GzO#yfz6WA&>;Xe z3PG1sl$*dg!;xYaN78YWUPz90L_it=QvdT@kYEwf<%Qr=i%(fDQ_CbEG(-!6#c|;x z2AUs9ok_&;p-3(BGve4Wp{#Ta6Rg2sWwjlWkWn1R(T!R_MIsbcWo#TV%|o02(;EVK z>c5()wC)h z7}U);M+#|UX{su8MAwK6IT-f2;=_5=MhSGGs&qUDj?8-W6X2pqbi~1=Y$r-Jnsa-T zHr1v0kU0TQbNeKXpF2D7DpFjg8V&(LjL6qwd<(<-W_3)wF?tkzHGoJl_sZnZXu?0a~7~(PouPtZONR;CucD32| z4gn=bR9Sh%L1fTs0;Y$0l7Hq}yF_KBCKl6fsbQ|8BElQd*i({J$Z$5^iV{G9l0c?u z<0DkDbKD(0!WU{96fw|V81!P0f{!v_55MXiN%lKGw9UEr7o{JYvRH6W_ehu|K6WffBp02-mE&mz|%R{LBSwX08}7`Ks#ta z$~4FxBOWp|oZf%S^v=_j(!idbr}mHuK~VLuN~8YWC%6CbI}1a$|1g4LMu_5QmK=^g zK%Zag5~J9PF@|>M{)EPCdiYTP!xu(B`_ayO?_iIApUVNI5lbmt=^D>>h70Ff_nn)4 z;amFAT#HA!a7NvARUHI`U|@`DH=R{qvhUMjjA(8w@ZoMr9)p{+^C!yx@MDt$wJly< z{Hce#Prfq!*0bGFb)Hx25O&hElhoE4%^XUqsv9``=$2Rlb8r+MOk}v^jCz{KVN2E; zV@T`L${S{&G#>M8oC!NtSn_A21mav;X}3!Y^>U{@ZSy1*(kXlBr=_j#aC6%=YaN%j z0=0QuiX-2!FEY5Er;Q4&ITYH$dUg_#t*E19!GyY4(?vA=)11e4m_dohwenh#<0l)B zTpWMm!O}zb?s@g)xBtx-7bZ9B3;VWD9GN|I>-dAW&+b20JGqy8O~<97&wZKnO582Z z9U9)TJte%odt6R^xQyG4wI9Eua^~pa|7-8YZ+>CA$-50(cy!Va{fn8%ttB{JP>!Q> zXb2_@P%0J_V8=CMo-~Fv8S48uwAP@!Gd{b&a&(mkJ2ASzCur!!ZBB1(*EYB5|IJPd z?a^kx`MGabe*E6SC*IE;{!Bd3om>EU2(nsH;u)dscJFUJT>IKH)35$D1HC4X+w>LQ zTm+J;agQgEYV6H)F=*&?I0~{UAm4O|)T7XY;s=Q_DPQe@3tc#$$QBCg5e`-$A@M;# z084V;ZopFmw4iBoWf!F6YK~#ae-yPs(J!TpQn;q`jX#3B0a*C4i@tt=Bx$EeCDbpH zkO>F++`6ETn0uwgA9(1R4tAm_Eh8raON+rXV3-)_Y7;rY5}ClR(dzI8#P`8xjUf5cg-_Q-^mb46rzDEy{a7I7g;C40T7l-1`2;1rer-fY_+K3 zV&rrOw4`$|NmAJ)>Udon(~f`qv8Hz9n5=;;+9CMcZBdwI0~d+X%mGJ^P}Z8-T}q1R z_ZV9Ui`vljpLUJYBfRxwkDT!w+Tbbnn`px!;_yl4Q%>t6IzT4>>Up4+{sNg@_7Xc9 zs{;QiKwa{vi=%u8D^)c6B0~ZU@U7~Cep0U_2Bm&6Jc!VG@6JAI4mGHRS{*u*Wjno*Yk-`XAw25cOf&UjuVn>{NHcG$bR#sCC{h8H;)8}LPk@)0yjmy^g?1P0n9 zmZ+pD5J^+gN5|pJ#ez(8?d;6i(9r5RLu2^B-sfX5pg>t&ftf$tH9;{W10$@CQFIaN z;Tk9(5r83h6!wL142sRQn0=kH>xV#TH?E--fDE9k)1R8ud_P6xr=cZSk`PpJ96!2h z2|J?I~n$(tlLZDfmZug|TTy&t)w|EoVezW45GrCXkMsZ(om zA7q0EeU^X!o7G?agXue4W!Jo+YLA!1ZQ4*>U}bc~suyEVFSnP=gW+UhW$=-+;{}pa ze^P2_AMa01_f%`Sr1xjf9D4noajij+*9|O}DygD<)oj;3_5%nE7W@u*iqWY5ckb{1 z!Y5|8?W@f;ewtnd+Y7bu=mA1R3+T{Lo&Hw)6z5nKBr0)oD;J|`Cl5x9&DY?qz#%Qgv0Pdx`!489( zx#nnbrFwGT zrYRi6ofUj2Gs5VII#MdcF{$f!Es#T<$y8yf zTGJ{Bn!wW17;tQ@bDPnqLR&_xZ~@GSNOGf?#X<8=0*$(^iKKF5kg}aIGLkExY`@Ae zz|#bpkrA7a@A9w#s<_^WWg(^BsF{jC2Ch2OnCYQ8J)clo{*r!;6dO zmX_0qDFm)m@Ar1NQOQ}c{r8wHW{1w4z5BsOmiF!&@CE?#9hz|ud9BPtrC)sV2k*Ro zX*|d?u*po^6bLP>gJgOU=z@?#F|^1m{JZa8PpE9t&4P2DQ|MW3G89Jy`M~2ef0UL-G+R*Zd6JXs`D=D`9{}?+kw=geiJ( z2Idl_SpK08G2~`1=GMl|t+ku)T)leb@>}Qbyz}tMQ#=N8IPGIS^5otlM^E2=+xFV} zsNc_;GNXn{uiqc`wkwr=H8xJtbCv4sq+|{-*y_z3Ibe(K()n5nRDp){0)gW58JlWX-_ zG5JUy9tny=G0Lf^Gr~~+)PP|H1Es{M`i2&@$5uSI9Z}n;6jitRk%~>Kkw8+W4#ahq zqPEH>BQHf|cNNERguh7K_r1b=Yjf<8G|C*-zIl9qFT{^;Pb#vP}572TNY_>9nT z2XsZW8N0CSvsW&azxFIQ@m8CSJaN+mT67de41fwTWv>i+z?-oPLBTM(+FEJsKg8p{ zN3$Ef@^Ip7S#6G|FnHj}j5xvY?6KNiXX~$jn_)nOhj9xfatbb%{{Ov1%)v^($M@h< zR3K=5NvNA#hP_80oc#FvMr4uEXr8Byx8}A#`tbCZE7O1c@3z)Aco1scVFQnjBi&?T zY6NM!^atu}Qgq_&180Du#sIiH;Y|oI=S_Z90CzMt^Ul{&cf|H;D0DibV~3|_7yB0v z*UlasAKO1Zv#)$`-vC-brN3~V_u*BBym>mg7IF-u%P016*9teMR=VB6%U4HVc&hOq zzcYOD#r_VjOy#KnWghy#L4;xI`v=pB<7wybl3JAFT>)J%vMbr^>KqJ zZZ<)f%^N-n)R7<1kWRon8L6U(@Eh4r&<_Hc zVH-EnIq(@xsJEEofRyC`D(I*lXkO-AkeSX}^-;rVGj&lP0PPm2xYet6xsRL;pj-Koc=Zr*V}baiQCVnHc#N9hHU_^VR&?W=!AejuK;@TgV&FM?_n$p``Ugnk zWF`;(s-3u?w^@VuC_O%1)ulUxETdSvsZU;&{5stT$F*;vMonB+?>Yj8sIYM%f?!`;?2q^7u$_PRUc z5h-F$Jh)!R1%S>1DgZ1Urx!qZP#Io9wlTlr^PlZs@q!!J^YW}_!TuZh5o275F4a3rxQgFZ+B66pJ60Y;v_V?y^7j} z0u1y886l_`vyackZ!r-h&dN#797PVTYbhU;>M4FWg22DX2Y{}9I2*{>w;@k$#xq~;^1 z5K&o-b#qY~kzOF7cp~Vb19B=dm9VGg0;)u`Fo{Uu%sH@D`NSBNITsJqV2T5U1YIhq zxn#|lJ^~e*s=Mo&aONweT1DYI!8pO_RwIu2zK zQ3OIUyzWf{c;zRhd{<}W#lCofE9Xog7oj0#sdiLq!A_jmC2ECoNjmZ&s z*4X_pZO#^#ms^WV-Oa9-x)cknJ!hB zr3vN2+q9%vKyT3VvJ+D}N~*~E7s$FSW=_{>a6YQCaIF7Ik0LyjEP>m{0_dicg*Pz- zpbiO?fXWR#GUH}P}Uz)?3duyJ$CNGwRHFK>8S&3{}8%etsH>xpch~% zfBD(UGjEPvoh!!0=1yV`P|)1$2M*9w1EYcOKmxlGmo5JAYmNTe@KZlD{?OTSn~Pj* z{&DD!X>n?FpOpragR7PIpB?|{_qcnoRbyA2Z-$UIl7KRs;?!RziG4mn(umeX_>qW% z)8_b=!_$BC(YfV?A-hs&MCqtIZ7h|3^zO+$UmE}UHTK-pA;}|37|7UA{yr?D2ox01 zqwoMrnL^aYUG_E8qFQ2JG96^t#{Gh0u5vB3YAXwsb4N?(4s_4$AD%rlI=1GVnAnvuw{kl;&Ld-V3#hCz%Y)e(NjKfB((?#&)^2%=SCI zljtyyFr+uz8K@K)-=+={%exUxfy1WiXguw2P1p>^m5B2vMvuO~@yUC8cb}ilG2p`~ijERH^P`=O+Q0qE@R9TNhi-3G zrahivM(h#mMii59x`FPDo6i5<2g`r{%K# zgQQplgdYlIZwKEv0f)3wx+tJ#j5yL)LdFGOYeALSwHg;L3^W|?j`_)R4x6Ybx1s{y zIA%8F;6^r{pb3#osbT)$l!k#_NR*Q!+dAiD#r0y0y0?$aTF}-Ig zy>{ZhO=1Y%>#Pm$*7dZ~n zsAUQUO9%UfOW{xG%n5zW9AphG0X!gruE+*vW(=(gDL^fk`#ZgzZnv|t$8w^}>(}fh zta7W{tdU`;6!@Q>!6lK55ztKGc z0$Dbl8nuy2lr%Q=<%ItEg?0fnw$N)mCx|9Z+$6{FmLtV{#B02<15XTpgpSzJLmK!W z9f6}3m&4R7i5&jg1#Mk_LQv}p4rP)zZ=ljH7fdWK&ViBBW!yG^q*WLqAh$CzzVR0M z%%FfrQhFHp;_)FK>WB)@go;?^94EA%JT&9GFvC}IMB4$0IN8#tksvAmNRSS1^u3Uv zOn)_jaX^t{S>#Z543_Kg9wxt|gyS7}xXz+BN+Cxh6udbWLRQn!%RRhdiFdiNPvTT{ zT~GZepd26I5a48hqw6l_6-8BezRD;4Y#|krDF->RQ9bq_vhi;?AnR4BqZ zznB2U3XFp&bTFn;E2+N%RX`DWL*M!w2zZdv@fFv2hk`XU!< z&Dn5#`p&yp_+=2oja|*o94}9K`^sD0we?oJ$pSJjb-%f`J{k0Ct8nbO$>fs{ zPJj8+3_53}E-yf3b)JyrU%xb8S=(&>%P&;F@g(m#sJ7YK!)!^u{~Wy+2#O@8o#8#Z$M7>(Y!{P@A>-3G?6kPK@>~ zkKel4V2FT2fP3sD69)h($c^0vE3`5Wfy>!$snXkKMBe=eKQg)ZY-`rzg%#YbQ1i0s zs5$G7=9;C4&o#dK^oZMDTe+tOn4w=-28yu5KE?$nurKdC8_gEW!_}kXX05i)%@^f* zyE$5!D<54b9axzjT5TL$o*r5m-+HKgaAnkLIQitMldPuGcTEP|J)?L$ONrZjs*^1p zz+8a3$jg}#6w5pIlATtUw^^M%#2&!PZ+w2Vxm|59Fl?YEYpo;-T&r+a1=deYgF?@G zpYTu1-QHk2psV0L9^GR{r;nVU{lxvHhi|K{E_Hbp{-n!VKAY^xEKIGCY1UznUTHYn z!PwO*kNK?EZ){GUd#?QAwegE@&8}_KhQs>8T>Ze(RqRU8(Z&BxVI5M3dr4 zy9mXYt=uoQmu5fxKZ zK*H34`72ox!GQVP@VytxKXZTWfBT~e&!cKFwP8@32sE>1+==MgCCf%S1-h6zs3d+_ zNr#-@b(i8(7RW|>F8R^TZ4 zPJhI0q`|KXM^J_slNAt4LcwS2?ySgYE2B}jr4;y+9i?RwGV9oDXdG80pzCdM>Oc{q zz{=qva&z8M8QP#bJkj0tce||MMVFl5qv43V*V;=fdllSdN#9lG;W|5;H={Rqn2!#e zI(6*)EsXjHePUzAlyWfGdGYbbpL^`_@%A>?ypp2e0{`8#te+Sq&a|-nuE#`B<>Y{k z0%DE$f~YM_opu(Pk-_9vv5;A|dV(r>~ zdIo*O$_`3k?#Pu2+9w8q0%}OgI(#CHOUHIAg>Xs$$fAYhsf2bg)rAxSXw4fQJA?KE zHXH{Y0JfkACu+k8^#$G|c=&0G*T8(GhV7A%PnDBCC*W6z=inl3B5ERD@g3I& z`GuUiN;3J;tb~ulgx2vw|5yV#;V2I^va+oGtG1p&Cr*$bRnjsgi@Figw2+wt5;+l| zi7~1?#pqBW0)d7KX`6sh7TMwJSTixkc$iC<>G61&wNWO2n;8p$DsA0>Jiw3Lc~-^Y zh2J|5(I&J?+9Np?&|okKagO;jlBBOIbjSJCm|s<+mWGie#}d^?S$0BRIQvftjCiK9 z2}LhC)0>(5C7cP5%272&HT487iv%j`rdp9UXkrcnN}y9Zew@+k%en?t z^&5CljUVa=6=ec1y^uEp!!%gz$^$o4jeK!LsW4LtL7;K%T59Ah8Yq$8VZ1~LlAp2^ znAwo>h_8mq1b5@YQGRknaTbVdg*aaIw_d@PzQFDyYr|}a!Frh9*deu04Ko8V{7rDe zYxv6&sxk@W8OrNw)R456bPNd~FtxU_xr)I#y;EE{N)r_%DhlbW8dVFQ0XM4A#0(`W zW)P}KACX9BTzS_E61g*;$vEh3^9q=dAps&e4XCcJ3hedz#1clakQ{B6kz!;5>cR&Y z9UAk|1p>0e?zAb08^t<19%C7e9h#ByE7fMJ*;~8$%wIpo!{<-jax1I96QAcYT{*OW z@4*9atQGGAu8f8w-VMz4+*+rD^Kvs)qutoEfB&1!7g^3_9R&pR2zZ*j^cSH~8!q+R zZw4Zj6YwPD=9Dh+5T~_gcGI)U#p90AT9hB;!MM&u=e$$uYZ1D2Qu9Uj2 zD;0HgonOLm$5v7iCt=}_&Ed#ZRMwAKK`xKFybiSgiH9n`^6BB3gVplRaK>V^7U6-4 zqjGyXEH!@bF9)CfdaGZm%*~TWxQ|oaBozTUija{&Cq+o6Z{CYeFMjMu^*twftV)F^ zci=f{M;K?u2GrkurTO^FKC`A?9gG=h*2#IQZfF6dvkXxXN(JYG|FGaeIA}NRtd_QK zG{5`8_-oJB-+QK4ZS{;8yAHz}c%~CJZI>?|EnPU;zI=^^Y>i~9bao9Mh-y5$hFb8@ zJE+0O*u{osbC;#~exCFJst?9nn)>P!qDb^x+4EclSONaN3FJ0s5^`st~ z>TVE(FvJy}5+Q>zkI0;j`yYO=@=t%F-I;E`xxp(ihKqBf`Brs#fjb@tt@^CR_=(pf zbIk;`dBP@QGY4hJhyLtsVn-H-Xu))-(SmGgNbceBh@eyRnm$0YXgcBjZspowe0YB6 z7k^@&mB-KgX}7!6ZnU)#f-y{pmw*tnUOeCaMBm__Z&qNkF+8-ucKPYW6O{*VE5Gk- z`OagL#SXqRnrv{hb(=Kl(|+AvhF|FI-1KNy-E4Ko)hipN$6lWP#pBhlJvV)4y}jAx z-Vko?FbmYH)1xcH58T0x)TIZ{cH1?skr8tYGEqVBOySsYI61PncHvO@**Ey+GYpUr z8kj~n`KRr4dmP@dNe4l5qf)4ROo#yqL4yDdef&I6>%qz{BS9jBmjf?sBqpciZAp%4$Jg5jaHAtQK@!Ou|2@B>?n3AC<4g`Fc3Dngfk(Ex@J zt`b0|DFrmMG4?kdaV%}@pm?6(inB|L=2c(chuU=G3Vz;e$`ZWt zRypGulM=FyNMxLb(+57Pz`#>9_OwdcFnwTL*O`#AZ z00w{afLv6#pq)UT`5aVsJG1n!tWYbME(yuFQ86CC^eLbPs=K0y5fLhSTS#_B!rjCa zY-oU$k&?U?2}B#Q@CebzNz+(_aF_z1!d($T1(29>$fP3F#Wdm|nr(2lROHC9o0*B~ zHiHk{h<$;w*?CYA)TjnIJd*A(7jm>Nr9vd0CL}7zoK|$vj$TuMvs8G3AdgX#qz;PP zQAb^1OACDfIr8=0VWyUq7`*I`OdR_sY$V8$4~ItRj{Ou~a!HjVpH--wF3JT9=c zv$65Q6HhGf-@CMTzZrAFbM)r-A3OT?tCv_kVM!6T+c($y+r9ZE{mF3L6aAfZv1>2NuS~$@*Y{j6{Pq(%ef4 z_zJbnr;Zc#R&y~bW-u?*APE0a4;_7a>TKYpglpHkg+H6>abeq6y#e) zp_&E8v-Av8c$&oFRK$YiG@%0Ga}uj0FNci8JZBEJy-ZDOS_eu>0j3o!n%jKJH@!9xTrV0yb}a=c)Q{{Mqmw9>};y_67~3IAGJskON-vP>e4^ zcn{~1l!b1BaD4Zf^6keQZ7C7D*so;X*qCh(N?-T^E5y~>ydweDgc4<742?i;e&0Rj zAo^mCB46WeluR3Q_1?z#^|z}dMwN{o&nJQpMt7{p4?qlO`}b5HI#>S26C*_8<-p*K zmCQE!dJeRCcG3wD0}~|ly3dFe_wDY*o(}fEsd-Cd~15~%xtAHyR|!JKf0$;mb(Dh5i?+?4J)8@ z&&UU6%qp0z)n~jtv`MOGP{B^RYJZcfGX!kAQ=hP;%`5UsEdBR*1Wk>HY00vVE!lD~ zDQ%4o?jQe?k5`_0b^7I}xa+pb6NDU@(wsji0T*R00+?@b;YlVk{JD=;AH92WY<0GG zq1Gr5O1xQZ2ag+0rf#`P7eQk#7{vYsgdV7B+1oB0=`| z0I!J@YW6R{AA=wg$$+DPugT&=vcZ+~lU z9v)b*RB7-Wpe=6_>(zLq^?0=B*wF(=Pa+ci886^v#o_WxuRiy^?~S&0=v>gsYdG>_ zoD5095gWCD9VwM8ixcTsj0A?chRjp5485F1F1k|tHX7T+B^o4lYGxnnh>474Ix`E8 zxkBL*m-I<7FQst0v51n0D3?{zg6YQ%IAftvRBGOw=-t8MFGw$dfQtq=s}d zSe|Cv0d*ZIhT@$O)Y+*F2Hd0Zee+RYETgQQ33xhx6~UGbSPu8j1*H z|_CLq?rg255UmI6e)<|Avk&?ABFWJgJTBG9P#Y`r5OMm-vT#_{FdMYJhDO8hdF+paS60I z#jy%di_>&D`aTwfgC0qjB}XOF%FF~Qv{*`J2qD3PQ=(hJi?;GCR_-!-3|qNIfcz;G zvG+6=*oLu}lLVT|NH3Cw%N1lWhZAn&L=h?jnC3(F0`G{c7wM;>$$xdD$PCR$FBwoB zMACV_TX_)##h?cjx1$E&cjM^keL&*v#ree5SAk5 z^-zNyIg}IP_zZam5;Rq1>r>ml3LDU#`YIA(B2QdEE^t;15FvxXfTt(gv+?VuJfq0` z;5<`562H6xL|#YilokM6d22mTWgOB*D(lkp_kS1D? zpy-SlKis@>`SMFIEiA3D5y%Us)%w0eM;BL?u3dhs(rCk`cXNAlZGGRdK#*6Q?bS7*KjMLJC{=(rvL&ZAlrySYN%z<1 zsfFC2ku#8EU>MDHKq|yp_4TAZ5im#a12y?Fk}j&+rCm|7Y>n(-6=LHg@&C%igvtRt= z2{N~O+CSZ9Y!8k+Lg7f#&3SP_0*e{8uy$6D|e?NsoDq>Y=I3KBOng1L#Epm zPF};?YE14vRbE+{%(l6Amd&9Aj+&7Gd0+BNuQtB%Y?FI;+YDVv)pm1=hS?At>bs+8 zlnel-5TXul3aA$N8{^y(7c0ht8V{9C-wJ)?Vz!D5&eVqEW@qw|JEs5UFKTaXmYdxB zU>{5!p@yZ9V>eeaDV4co{8HuRR`c!Y-~V9wfr}MnBr%wKY~iydO+Q>y7>`!wYkTGy zbej*oc%pzxf$eb}&`n*Tz#;AseI?j$q(PtJimWjuMf*y zFJKpyj>e>F!ab_odp%troH#uHcOPjy{#t3X&)w44fJJT=r;%9sz>+@2ihB*Ki zA-cnhXIhKXo$1Yiq$l+{lN;|uV1VVz&-BZ}9wc1o%qHJ|sr)~Gw)W*0s)JEuTAgR5 zw$orP$YQp8xL`mN-tSeJpYCkUKXIw|um7ld*YTahE3uiEAYtr(N8~6 z{qj?lKlxsHG$I@L-*GUE3Sl9~RTgA-02FtTv)SE;hr*CAMJJ3FJ_Q{ObxB>^x=< zKUN@L*&HoY)eIHE>|bMhbub+5nV@7^@DigKA}Uj9#Ipx;cuIl24Ru_WfP-DSQkz~O zZffEA!j(>I^}r!+VB;-+UT9;^J=ob{0x+KBS<#gPM~^M+SsIUdpFKfc+qt>+@>Ad6 zx^bi4Xi<_Zrv-zps2GDH;=UWsQR`h7_Cg>QHs785aa|<(TbftQzEomq>**=-}wo&^)6@fsa2USm4Yj zd7k+YDC=tgCrAF85y8ofE9CUNdPgehYr7}41@LQcv%aoJQ=q3NNb3UNi zaZc^VC!S<0j;OEo6P7I@qZWRb&L3IUymEt=nMAlt6bY&yXE?H&m0MJ_ra;!26 zp|t9*wiFm;!qud08mg_=81|Hs2*h|KnU?)3yFiWaSS&+Af?vC zuqf^mDJbV!sklZWfv}rIb~*)z)SNNXAh>>*dXBFPpd{^*JLKf+Crs2CdY-hsN^p!X zyU1uAza^EZ3g4lM{-I8`fE0ZA$AA5TI{8*O08J@E7CsA^AsPQPSny{I8d_uyVSokf z1RHPRd;Y-`@&YXj=R)@Ew6z#3*@QouP&}Zio(RMwoNCdRKmN;a>eEc;fh|;^iyT?7 zF)aNDXqr0-9JSzXTmFeGpJ%Xl{9F&w;}_5&4l9 zhYkB{*RBqFJyxR?WIAoNI=ni7jTg8c_A@NhjSt<+nHdFfr-TS!NCn8jkIp6Vq+PW# z?o~Pl5@X5?m$(C>JO487sE0$ao(B=(p$WH-i?Zeegs{Zrv$AF=)bJ)^&4y^b#GEt(KG{O_}f#G9Gn@cbZInNgrRVIle z5fWFK;+5FV&W*FBpZiet!*^F1-A;Jy49c+9>dcy0WAs+~$`g}6d2BqX^I}&gcdm|_ z(qd5b14q5be}FlgG@?m&Ws*S%88*NEboGs^lM1VK5+v85 zBT^EH@C02=ieP^d^-Xlgp)$st7Y@a|Ki zdrmV|fM^lja4Kx2@G*Iz10qm>H|hy&@qhT1^56Sm_T==&{hD{EdcTM8Gi)TaFg!1n z2d&oZ#9niSyYu0%^N@oGVq``MjYUb)jn`sWu)Mlm`*(j@e(u#ex4!Ux3trbZ;(c19 z9kB#u-s9y%f_(6}jVramZ2mueul|d_KKv)YHu}}y9{j7%4?g=>)h|6&`=jr+{_nr+ z{L=3<|M_oBzxPT5iy}4~ADFLT45Auia?yhvP#p~)xlp}ult-yEG^SUk4m;%#P|qjJ zyIIEXSrovtYZW$S{rj(sfB*A4gUuQ5%Ocv@+EZO*lF{f7*dOGRxpcH#?L$(&u_u0g zr}WI5_3mc7y|B{maLI)!4MQKU8Zg{6+!*V!84K$!@G6P<)?d9;`sNF~kJ?A*I}WBJ z!QN=jml1mlxb1`&NhhE%s-0&%A7{`K0;VAedAG!x_u}K!M>?Z}+PDu@uqTGD*Jn+Z zuF@kLH8jE_cpc%ivcYSbyfj8VkOvcoDJ*`EW~2GJ*>-pCcfM7-bQLH78KRKH&2a!2 z8dbC2?C9$7AADf2r%lG<&R3k!HE<==>GY6bq}+OG9KI0WFD}0GO<}2_C-dRRJGnd! zqJ()@ub|6Sh@gR{!eMR9NHWJ3paCDMYGS=G73?kx#PCU@h=Kwfx#RcXfe2(7XHbq* z(vbO{Vo_gr!PfegCF#nt_*@L($qwb}i(wXwOewho^3INUU|vhRSqTQyY!T5rDk+S{+cSz$Yt%XOMt zdBP6DW*&rr1CB04G=%GJNI3^=wz`Cbd;m81nUQR002M$NklcNZ zWH@qY-SCowWNdGbV|hHKDA%oA8B{_0Nb4#H+wtpi9l_H;=R*vxQg1$u1PhnM3CX(Y&7t!5=2RM^^?e#PZENy1+^DUvOzOk{m zJB-1vp~S>0csad?AadD|{ur1dY#>Ld7*&mpP6vUTKblV8H)&jh*pNHKu{%H zDX}IJBMa<_TF=OUqU2I^Q9PlismO*H_=u76|gA`K zZ=UcVAEA#cDLpOs0JUPfLny-T&@$su08#Q{0VrHdBq-^9kX#xbj8+aF zeE)y;lcz6SaMZ*qoVnVO<wt-h?-|JMwn>ZY&3F(<-@BLZ#Bp>({SeehZ~pR;0V< z%|*)x4>VdWc6KVqxOC(46*dAjyz7#^1|tT*tNRaimKG2!))8jSXcXwd`h~evK`HPE zsnL>?$QK2S zmDDw@=n_;gpNFgkJf??L;X(hC5BLA+qdN+{W z=$k-wKE{V8ko@C{QUPQ9Ln(|{CFAlHk4Tt~&K;kJ|41B6!(~U!66l!^!IW@Q%~0MmKnr zjv3Vaq1gZe3GF|+yiTtE(79=+h12I1LeNDW{M;vTf|;OsO_Pu?Y-n8K9ks{z)_?wE z{nL9#rNIu%rP=7qIGK}P4xkS-qku=HvS*Qv36Nnky@QPc5l?dt#V9p0H8D2lF*wzB zN@*wcZ=F890!c9lPqF1~E67vLj_QD}Vik))${%xZIl? zcUBq;%kAa4`K3m4wcTEt+Zc7e@ObMFzT583nmmPuYXIJ+jzC_`L#jc8gf$u;+p}@+ zDF(RQUBiGQjzb_Ll*t|cNC{)3#DFXP^UX`^bN|DCnE%W-TYVN*c_T+_?&-Ihzxuo7 z-}?3{Z*X%2#^r`GZvzI1j0q~W)jcgU4AG1V(C0M(tUd6}%*Z+_1_|Pf<5+#Gw>E~2 z$1d^45-u(=!eXO1asV1bb?mHL4BX!e5bU33c!xJ@J5#nP*JxZ@8%ePO8K%t^rl($P z{=s97!LVV9g9S#ySL`KNAsH}V;kC3sdQbP!_iUBNEWB_(M$)RoGY}FAlOU84x{wu- zxK@EHx@FCTMUZ$vhB4_H)K^YDz}Tm=qdCK*1l@8pznACKd62)1$*U z0CQ0|7Be;-_#h|VruMm(fdQ)fj~t#~*~7+A^DiM=F5g^V7Y5^~1Y|so;K->HcRldH%F*MP zl52X-y&+-Fif5U#q3Wa_KT017GL&crQi>+?S`NPs-LM`HFX92*_PO>;yaR96-`Pgh zBCu^|-$b!=?Aj?JKY)gC7YnQMwp)RPXB8o6qE?n@tQV*7EH$+gSnFh*p)rgdM(E^* zJ8~CJf#8%Cz9Kg{)Z>f;Z4S{V!;q*) za1<+ug=fGgj*DhAvsXkaD9XmMy@Qd|q_N`5JZ8ugM2HP2^@;)F*l8RIbZy(H2M6B2 zg2BkWv{Ds8vRpeNsG>k2^vzBpWB0D(;=r7MRyttS3SIuFFdzw;>77cUCCUQr35}4& zI%GJ?be1}VHz-aw#MArvDU804f$Oo5<>ZVpN0EA$p?PXrWjII2&i zW0LJS4v|1LLtTxlHN6ZYYiRRV@6CELL?NLSZN{;EVdy5BIRttI$PYXUF(@~^i7t$z zV~$Zpu4;pHLWYA1kpz%C4@;magml~@85L&(6GjpdE2|PA3jkIJNtfSrqF76G&L{Y z0Twz+i#7T0RMzI64sLuChuu0fRZXbKKSz#06i*w(Us#c&IgIrX{#uR}{NX2@I6@ZM z;6R9o6WcI-p${bP^RjfDiZg5_o?lp4SXqITdQu(IP%H^RRR&cKE^q}@ek?!W4a+zp^S*Kyb3f@7SOS!sde-oY*DQJ|}dvz#XGDSElVni-4*ht(^lQT6rOy&4PJR~C7S(u99=KcbH zsYx3t8soY|C{U7=nN#{U4S9B%EUT04>czw3fB3QC>dJK7=M`MKGJUzzPe)?pOrC7h z{OZ%(m|JPp%_3OD3?Umo5RFFg06A^DDol@<$q>2@L_K5!MUHnvV4miu6m^J8xrGFn0rwE}Sjja*P=& z6Wug7qHKjh@D{pxJfSx2KXSfycn_}*qB%?K>Z{030`&ua{c!;X`D&w4xmgbU{1fLVZoCF5xGp`Ij0g%dEe1O`NE%{922_z%A` z`)@xp{%4;l|5v}(`la7&KXZM)(rmEkNo?tnqz|uYLyi>MoN%i|t5sj9cjgJ4KF_0J ztut;k>@2f4r}_Esl%Ic<=kah`s*bICjsp2tMM(_XY?n^%XWVYiVQ_;6h1%&cM3!P( z(U?Vj_R`iSl}_`@_13R_zWT5KeBlqCT>PcqZ~pAR82+DsyztbOY>CDd2$Tx@bCA-s z3%BLwY-Nr&K1fV>Nkv#L!BI#R&2T5~7bbSVuCd-_#ltEN0I06`lF9>kr)%3JdIBz1 zXWj?7z#eQtx7d!LN?vM^EmiH8{V;?W`9RTrge=Dcs(4`o4vt`|Y_Na_H%pY)yWIN> zb47;^4upv-Orm%MFxMU%!~sj;?ia$J5lS?AX!askeMtbz=1#Jf7+uBUmzuQ1fvW(`j`p= z1;Gzy_<|z?Go%vlj_Mo>U=vel$eJomN4hEF5wwHZVAP*mSloZ|BoB*FWCqiXI(^N? zwQK!;*G_#_T3lXi%yn2}g=wW$A8hx#TU&5*6t0{wswZ@YEn z-~p~ijlIF2=&ihS^~(D7wS6a!-|@iv=Jy?B!H3+!v?JLx@roJ{V|5tFLM|Apv7nO{ z@ZA_$q;%m~XK}I4(`GRqr#N^&s&ARP3M?`J;J^Avp1i z2gjMrM6l)?Ei|DYjyrbYwqu8@>Owmm=Tz~gXUe2gMKbDr*A>M z7}5G1#Pf>eKuO^j*oPxn>PFGBXkn>!6b8*<>X?pF`*BJFr_L+YjF)s^(6k!02@1~& z1a;%EDyH_jlIRFb5p!thTtI1+G<1r(h`@Y~nkGEKLK+vfcKtjuhcWX?8pB)Pum=jJ zQj1q1gUcAo<6wp;qa-2gag1d27|VtLH2_@0MbSquTJE#KMeXI z0r5t?vAnu^sm_z?bOJXqTaIFilCHx7F(h0trY!9k*=PG}>$usNcZ7C2$IqTSef#apD=Y83{r2;ZKYsnvC1FG@Jy7gQs>9K}?hYwK2X{M3|ECgNNHW+KyuyQy{X^F}v$H2&26@p=q^*dhtc3 zrV#a00sbnoCXTR-buEXEx+fuVo~EdYYY0BBin`U=)()#&!;igpa^JafWrqzT48j># zfdxGoTe6eWh|iD`8|ubx3>do<>SNZGbrdO^_xc4K0NY(=du|q=i(oV86);uVp+3}MAqCL{! zJ;Pp!jXxi}SUSGU4jMg{#)x4?jXeCJiNH-~XHJanIyrj%N@>(+v4pFdS+!$(3ZBlB z3wd=XyohV`<)NhyogaPr;n4zv&}}Nvxn*a4;Ys8f+{d5?HrH?wH-U=?gef90!(eEO zAjt-SU&|sF7GWD$x!i0|cShB(J>8s*nU77@_Ltt-@u7edUcA7}&ReVimL!bfNK$M& zBO9@Mi@_9Ru_c?EphgDc9b4treD&>({!=ec?mEF{^x>2%FzlKuTv49dh;WMf`gAC&1uU z9*xUe1Cg)fg&%0ol$BWWwE)4Ky-duYUVVMWd%^BHUg_|FtbVDh7%2JKTx9Y?Chp)2AvDhLTZNvyH>``3;3yAoAse{r| zF~wLB;R}PARWzDa01i3$03tyWdoz*#sj1(ksuXcp$Ry}Bi={_van`W{D?Ko|~L z!q$orRDq!w7&FR<1X|A3PUX}^@o4vqTT^}c;cg9#W6%-ZUh6F$M4!eucKBBnI6m?P#{leHm)RCs~i3HS4-84ixYC*P`Qc4P~%u<|@H`dWI z@QpUG<4Ck9+Vyg1qOrkPs0%s_6KSR#)?&cM)=Ty;qP~KoEpZb^GVZA*9?D}cp-1nw zvuBIaK1@yjFxkOf5oqW0WK;&35;UN|+73w{`K)7Sf&4*&A*nX^!^I<0nOB10l^mnf za)j%f`XyHYj&PDMaDgU*MI{Hm0YER%S_+RBs7=1no?~k9OI0iDWQf482|z&M#2Jdy zo7y6pz$f=zp`opFX(9CE1_cfIkg4r8Wdhh%6&t08c=4Swi}<4VXip?pIdzFt5Jnti zr441O2+}suo@#(ASeEL5BO9f+HWA8qOK9j<8(JH~d7?N6B}k~8G|U|chy<|IFCz|8 zN+X50j~k|@8$f6ZKgv;wFRT{` zpWrJ!ZW9pTNgcTBXQ~v>%=eTt`vOFyS8DVoU8y~i7C%U=ar4z`s^AC2WyL?w360={ zt}#XVyRyNO%vv3Gd)=EiAzI_P7A#~<%S(F?bmlwp3@XFLkYP9^F`4^~Ob0EZ%WocH zbAB}dytuJ|y}c)HyY<9vxAQcajVo8)e(iPN@l%@4t*qYu(8G5>@;Qk;kf#PE}s{{@joa7~Haf!7{1Lh-4KL=v4zzcS5X zt^zXc#4qjedq?uXrN~h^E`hvgji!(RJFHp+aCD+j?rhM)?z6%4mXm`=?=8<&8W14gJ0yC6&}l0;*+(XD;|)mfjLUs>Y=Q>>Tpg_zeMBxv9|B8|tH4=toG03=~> zQZ}iNd&4t_Cm+0(XW;PI9N=U~=O&y)6T(Or$-H(n9`9QmeE5!1i@OGRyr!`0H##|b zfvDkSPC&vzZ@g!2@N*v?-FmV-WwJzOtVuhwQJRddTF8D-a{n0PcMTe@6ymEgQw8B2PQY&YIg|fqNo5s zzITk8tffyY*Ig_(!@aludbTVKT`=%D$XWe?yZNY%Fv8jIt^N z^X_`9vDU9%-5fJIsVq7|HxvL|ck>?yic=&JuCI7TIU@ixAOy|yoO1I*3U&cEI&ZGD zoH@v=oiKZn_>}8wW+cUFj4!`4ed!tx4Z=SNLN9sf5gH2(ln-!ZZG8AmEs`X~-UvaD zSm|AX;OvtQrhx$UG(yEZV>wefPYkTa(WdwM3Fc9|#6{D*Is{`^TE zC&aW1f6+(~09in$zp=N6FR1h254e{7kqJS_yZCS9coyHPwhcv1daD+tyq;P2z~tkZir*g z%~Y8WttA*VJ)^-Asenv zDi{t2!#>dwO6EZUMFr}Ln_5hl_pTnj<-+pnUUufJ9yxl)gAd*F$Ol&rABH%W7uK#{ z?XA62sx^!ds+bYp4?9`qrsSs)1q0fU#B&%$D24Fl+S#@;AQv=c3$U09dXF0(i& zb)Y2xkw>_26G_uPa9FNM)xZK_e}1#hhB*WfqPz!**5{4fq;t8;@P{>0C1mIo_i`0&KIpsm%va&t~Z5)t_`Z%QcS_nm>*i;C0 zse+%NLc+AbC(>$mPD(wsb`4z=XGTpukSJ)Wy=m8hTtOUA8}JZs&J_3M{kXIa^a7?4lK zTwq;2cDP=vG3!9ssIBV2BWXVT>(z}Em<<%4liD8|j3i{9n!uU-$EP(!+#yw(5ODPA z1Rlr{pV$P$X)3f4Fl)=r5To2P92X`2<)0p$622>r6=_edm2IpCDHoSGU*r>QNgFWX zZ@ibyTL3RP|NF=Xh6Zqlwy;qzCZm-iSx4^t(H&&0t|8v%@s8!uqxY8XJu_nr?j#s1 zLkVDvK+_P{w!ZN~`SLs5U&p(xXyzgd4tfixoG7l}LjE@EAM6gEy_Qb0QS!__$A7cZH3x8CT{ z`>Xe!ayHGnJsBkUn=Js)AFa)i&oSiPWRF~!ojX!zj}wa#h=IW9j5fh3Az!KYy0c2J z_vrh}Kl#9bVK}{%*fJ6;LK`XY6B+_Mf}lPrz>%va5kn2GN`fP0y(>S3xzJ3$SP9w; z4!MHSU_FtY=?oSJ-Yjc7B0hhV1+_EYKXT^oRp^0k)IWJ^kwqL_KEgxI%A*BZQA%jeR%+ZtwYgtgqk2{8DN$`M(+KmadFIbdoiv;`uovh9%c zQltCK!YWvDo+g#*&2D+a_oFBne#b-^vmz+Ig6X-Do!gLnk3qS7>BZ@9f3dc;QDb); z7-(SrfP%JSXizP6duLDdKmDQp>QZTIM_hASy2z<~;4kt7grBl;9SN`0@P(W-!rU>7 zpG6cvH&@Ah-6%+%{ub1@z7lc+*{M%zR5PxQE$g5$2l3R)OcOCnZUe+f<8oyr8x>slSdn)Kyz|c0H{R%NZ4oZKd9vDWp1I@pyB~V!!09t{OUukUhAeB1CfvGy^6Z(p z82{r=wp zb1rbo_r32^{!e+!_kQn(;NA}`S!0@)D#o7DMSV{Cf<>&CxdckFGInA6HlVY8W3Bga zL=(KZvF^xNOvt&26fuEIx`LK*8r1Vk<3j)^l%s&cJ9gA+3YHgVryfXQHBUa{ppGeN z910Sdr6J*YfP#u3Ij2brP56-v&Knge5?`>%h6on&JVl{Yu;mC+^A3F&*&kDKrQ}7x z@s2ag$x>E>lFn&xE1eFg@j?jZd|^_75FtzyhF*dm2yq9B&|{z%n;Ynn$xf|neFM5m0U_Nc(B+(*Et6dd$lK8d1FJaJ@;YG@1HU2^}Bw8&EEe!Z!3Q#r3SP-pq#BSCoGDozy%YBjR&F0VW~kz`~^y_qnoBgISI-KTFybyM+rSU zVP3V@^i-z^F9kqFzmp&M1va`;A!Fgo`n@m%D~%8ZV|MWIU?CDb^=d;J+S-PyP%X@= zM6sxLiLoJ444_59u#xT5-wFY(tq$f2&hTH4j8+^}Afe!7A*_CmL&c{&BX4D)COJ@h zoB1j2xHORGhf)>4>AtgshAUp9RkP0q$_%-NHet2E4BdCOKC?A)!5ai3ZNdc!6PqSZ zF!!9pP9wsu85`yGhRyl;Bac0@v}YH0Rk7Fm;`W_K9(nxev(M1`-*@~d@k=al6XDAB zD=&ZLtLNT$-R#Wsxz$$L_$N3brWSm41q!D)YSY6bmYQ0W0|@6ujQ)*_m%1zKwHm9{ zNvDHqt+8w0;n{85SWQ6)i$UzKpu`w2g=8~QUpeWnFurzAa z?LK(8{L3$t78fSN4KQZPNuF*Qu0W=#bh@a4>Qr>iZllgKuw2FkQNu-FVEqW}bsMc= zUqE57B2@U)-%!UX2#jA|T^rt6^GYUDfH%@QR-sljrzrP+(8pFb$F$}soYQVEqzkN3q*5O@YNOQ^c2*kx_|v1~ z`^u9pWd}@hXb|)vcAJ$O%dIbbue#Q$Hn__GVNl!uQ^Q6*;YCrE5ITL3ul`Q@M9<(3 zT6}@-q|qwRG|A}rdNHLfSp@9#%l)ooFr4`yEfHN!c9?ILxf!C-?UP7{mBGf*L%ov+ zxnYUj)ie_~3Gk+Frc*e(bpgBVe(HUdA2~IwbbDPMAK|(|${-*fQ)BZ>t)|mC6TzP^ z2Sun21^p|96&}~cBRHV^46GOfpjvzf!S!5qj@r@N!5FlS7rP*O=?@^0sG^3Cd6J*J zo0>REVgCuKNff%VtUYzOrC3BEisouVJlZ(veLuP!04L*dWIZf}<=$u0h z$oMP7rYcdImJ2@1H*cT*%Gb|*@AdxX26JKkPOsnV?Krsq=?{PS*^j<({MqMbXBJ0n z)H51x+jC&g(FfRxi>8xnFnT1a75Mm$P0R=r#;bhlul|p+WZN+h6X3}v$ zaCD+AfGJ%_M2fG-!SV_Pm2|z6T5Y4FG6Kcdpux9DY5Ahto84Pl^*l=ARYhfyl}RM);OGMh~TlEz;>`)DyE65QP^y2wq0g6x7~!4f%iy zhdGqYNmQAPk9W_lKROh^(!o@23pgi#h5Ns`H3_ZN$!e^ zSXB%~)*hY2q-hEP5abguoijn`C<VSzA+ZhEtS`lvi|_b25j zL!yl}(wV29KvgwBdgmuBTwS?yYkheQ!rt-VVyXJhg9m4~EwM`#GbfN1S4j~H_GZwZ zbYVC^jNb~6;3j=$)vJv<-Ti~djx)E*)4N#jv1RAZC!T-*6VE-rxO+DXlU-*w7@U9O z^s8U_>ZP~eWZ{0T+A#QxR|PA^_=OLO86;RZZ2wBTz>2<<=pr35z_?bvcjLyL>sP&~ zhVzq`%ag_J+xHwkT;fJ6Zr_72kOGJ=!-F^Fj!&VDpolJD6-}%{@DDn0psbL>DBYgv z2{bkwK?Zy?1awyjY14a%>+mgRlC|(bhEEiQs`Nb{08=RkluH+ehm2!PI#LuF`x%N>dJEkRn1Pz_vN1GLyti$v^o}>Cqzt zo}h!$?#`po-~w?vZcb`_|8ni+cbM*M5yoHYIhgbkp?~j@{*1XRI<&n%XpbSDb6j%oZ$%~w&3kVnKb1sPZ z94_R-wdQB&Bw3yxfK>L{-!mOZ6C(5&&W0Cg}-a-vuT1k$))+iA5fl$(7Bh#2SG(n&PXzE3DiyVh_PpC4cJ2%&Yl9O9hX46b zR)6=uto-^UD!Ug)KlS|Rz^=-0jqnsX+YzN8ZY>^d zr>R3l68In{PDp1Vsu*2?85^Pl_?xXf!e=stUT1X_0S(IN6*^~O6AW(_lQ8G~pFhBe z7?ek(K*cv3Z(%|iO8S6oVWu%)D^Dd9Luy2zNPx5HH^@m1w8!a@7y@2|qBu#^9EGr| z&R3OjI+kBNoXP14XL)IsMr@Qnp5MB#>(KrhTZ8qu%;^&+hJ*gyOBXxt|HGmmaxbOF z?&iZ_IgzWfq%qBTqx^2N>DcX+^@BMdlh-+uVy z$$bwT;@$vm_vi8!3kr{%Jh}IQ2hb)oAn5o;JTVM3nTEs@D?|KPP&BA3xeXEO2Vg2G9i6B&2{9(Lz=8$oL{)Lxvc!?*oBq=~DMV4; zpk_m7gQ!soJ4EXIiX%}M!#eV`D^zfYnr0Cn{&_;P8e0Adqkhau^x`Ll=yYlL@HM`a z-5(a#<;V~o1E&m+LL2r)F@hdq;FIJiu0jwEAnzQ%IXXp*sR$)y4#PLDjM}()5>;qg z3~lVFhqIpKh+Z6YGOQ#Me&vWCQ90z*k?L^PU;QBx(NAQ_a6#B(z955ql#cU5Hvc#* zFt~Iglk?V#UWM_b#|)0dqnZr(sy(3Sk~i?>2Qu15rsHf&B~^}wKBiGnhlwGIoy@pX zJ@estzDUW$O;q4Qe85X0*cobJB_ofjhnl6QAXw7#SdORDC>^U&4!(#gZTw7H9)Vuw zocQGl9nAm?heAUL<~IZq8KUnQGXi*|C}jKsO+KRuVwhA1ztgdV4 zo@N8u(oOF1E-)W=Ds*aQGW0S4Gv3^p-h2yd_Yg2wAs?Orl<)K-O#Qn=OGFS@s%2=NMC-I zo;LkNJOatmGBQFBOhL|sw#griBmhAK>8z}+-Mxk6rcjsuupn~R_MN+wgSg3MPH_<$ zrVmj=YdY?cgN~=T8j>Hw?fdpW{P^R|nHgqKpvF{Mx7%kYHFhVVZDJqK;l}2fS6+SX zYcJloc%{aCNVBb8P!$?&Z_=mlJyH?>ke zt(v~YS^SU-&!M7g70olpC616(FN_cqaY%oH6+~phuW*KrQO8e3EY2v8LfX|zdHAC; zx~o0RO}+{N5XuHP79&47@=alj**Ob=%yEXGAW|*Ng{8nA7wPsYvon(q9-l02?Tx!d ze-#O?gBHob7FeHXn(+G)jVCZa^_{(*peNcP6{G zRzCf{!Q$MA$J3TZH9FEbr?x62)?t3lI|5@pP|D1ZhV2G2zZX=`be>Ak>Rtp{A7BX{J@( zx#&7IJ-0}Uu@Wlqq=C+G_{G=Cx9*K=ZHBSpQ;0`dqy-=0fU_*1DFxmPAbk#TTzf%$ z=u2Ep8B}{{MFQccxhGf4!I?V)Dg*WzB7;~RB2W)a9nz5I7Rgd?v(#IyJb7aD-~W8? z!9yb+e@4%osq`Wpv&j+9iF@(8<$v*|(%p?JcS?}huqEqoAnfW_7|Rs7$Sfuo{RMrpI&u|k&-iP&An&7Km5q?(qDhB)<)kB zb`{s8kR&HW2lU`9yvUXh(<6x(i`z)(M}glw5NDykh?Y8~Mj{??Yy^-F^$Vm6pVe<; zR*(Y~{$!xrNl|wuD!`FLnQ76Ii5gADR~tEi5?dpf%8>@X;toHH7w?%^@;sjISX|+> z7}YdP(iRj5P^wsMS^*J|g!ybYsrLTy>h^v6w(aERF2)=@-jAm+SC?#05d&uF3zu81W~I@jS5BA$p2DIT1~23*S{eMO(cpIBcV2(s8@_5(&eTR=dTyM>?RS?nzYjd;W%0R~89lK@KrR7xzGrZj1;A{5rdpvEZ)O=|jeg0{#f zrX~~&ofty@Krk~YrL{03J6gv*iY7p7aE1>PB%k8%sDOGpgaKi_m<5k44yl-Y)!WR0 z92!#3@b+ymx5G&_$1PrGu&1t}l^znB+jppMkNtfo333CIcYQAa09XVd2&n{f6%-Qb1C}46H$}^#FrpHlGW9bViqL_Aatm(G#D)k1!OX63PJH82A``thUED6C zy^ug1kPeK&J@Lq4z{9^qPo@qCC_;7qkiO6Rfg=pt_?0T=$?QgEL=`tJkSK2<-lS$cD zgJdwi`tG@J|JfIqXz6XNH+?9h=FNm!(m+&7{u4omF_m$9Zm!*&u?e&>VC#|2+VUMb z1u%70ATxm+E|+g#zI5-_O|Ir*M)SpZw6uHA!3Q4TIgpMb1cI;?15Bf%kZ%Pa-={O6 zBYR5qO)GrYUkbcEgF6aD92F`YNtYN`%zYvZNv)nq9Vc9L4VefK7QmL)L=OEK@-l(E zI)Ph`@Rm=;OdfVoev!-A6S^M{|}ugbG~WOTQbJw_S-kh*H&Hs zkB{5?LYb`JRZm5iSdo9Qnd#C1Hofx#7(%YkGpGBLk3Btp?${c;w+z`-1i$glt9rjw zS|3z4hb%F%1IO?~YBZrd(zMcNk-`4$-4C6pwr86ImTzzqDpv_u`s1>*YGj&Q zAL$?3!p0(fR`k$y_x?&Us_m#ghjnx^C1TN$wkBRGjS<6NCP~eWnUW$IaSo@`G5KnT zhH`qmK2r=$OaV|Ontk}%VAAQkE~!N^CcWJqM@)kc<#?PI_uZ%U)Y~D!62`r^W8zr|9i`F6X{7*DJd1oWo#yL1UWV(e%q zeUQf}lR1e1Eyn>96P;U*uMfG|fM-eIXI{;P5w4R}QuADHs8)M4@2ru?I+-DN4UDTz zhAEWom1=9N)yl@;xg*_w^b5leKi+6{**S-6AU+`vpy~9DTg}(5wSMDIDsQ|yuD52Y z3`KoLb^5*zN)arwizMwf%>eRg3$XQ@5JO8Pf&-%CsY$4V@m!xAd5~MPp73GX7qX*ZpZpcx|TZ@dcXMLjS~;>Oq3RQkYTwam@##nnF{fE6D`Q< z-&mcmTE=ig%DC!~ zdqbStgNc6Q(_6KfZH||IN5i@8i@S~>q~T^+GSzqaQy(ZHC@1kB8o(JI4Swm&*)!ky_MJ;tXtA0+ zA_QL`yu96v>8e`4)4g)$-CI|$NU(}K5*zkyrKn!$t5xhsA-Nn7Q&S){JO6Co%VTPm zxHisRa6Iy_v%YzosjCgP8?sracZ{RZE=Q^)60#s#F#%v+m%OcqhF5<1B0p#Xl=xbY zFPP58B_dH;$;6o&#E?>PNOZAJjw`rLi}@*Jm0JUHmMb@!)&pXePqG5)nZGQ8R7wJQ zQ%-*9uKrdq2mvkv=yO^>M36)YIl*5F6XFJeu9A~}quTU}fJ)d{Q3F;wYchhWDTPjh zEvD9uhl-_4#8qLaf~>`y45ZV{Cx@b&eNoxkPo$h2Y=DdgZ2~`KVX8+hQCZVc4l%%+ zQ?M`r850mLl(w#DnIbI(7SbpHq&}P|pp0tmOq{0$0WJ8h&FrFjGiK3n*pG!(vaG$< z6oHU4;*noo$#SyQJp7O*$UzsA+1KF{v8iH7B`hpJ4FgZNRthB3B{-_gu%(}+Bxd-C zXWJte{%61l1Cy4B>k>JIpQLl0;{c#2`BMOXK-H_3l+lEOfowEwX(M5sei@y?hFd2!*M189A8W&_kD!8#Lv|7kbRS{>{r5ZeBQlh-O`EvI!zO-%ko^3mJ%xzt4 z%(R&1@AP}D(z$i<;Uf)<}n>L!sDmO?LA;fU7THRP(x%Te6+js9FPrIkkXxyqd_8oiZ+U1M4FI;x@ zvn~b@F+~y891=YJ8DK!u1md4&h+UD(PY$ps-^9y*x`VE6!b<_g%ATPaz>#Zo;ao%^ z1-&Y^#O4YrcWob*=~flOU`;G$*RMFm^=jlGx_~-0iGIB@wY8e^#dA~#RG53s7g|Xh zqWtNXIFm22O$|QcRUEQc#5M{}Nvi-1++xq7&mJqCWOJTjcQ~Ho8M@{W>dTXoOv3RD z*U8yy+>O_+H%Nxc7*z2ybEFZ$I|>nfNF@YHYcAz*rQYq(0F{pKug}k~ar-puaNJmh z#{z?Av%0cYdFf24vpK4@8~7LxYEbCJry?W{v<8)et{DvF_-Qv)ltHfF8SUL$`pNf~ z=N3o()!Yt9=X+EyH~ORD?Aw>!^ZV3sc9Ro1${Tw{YH zqm^8a9z2q)G4B8DhsQ5G$+i7X5y~XK?6<%`l(YzZxDAOXGkKH{2teTQfE~6JnK&u( zGiMDU+zlVns|3tQ7B_LU#qiN$=mo?!E#`6469{wBNmddxp%}4VCu5sjZkC5)eZqrl zbr{MHYK;m*2fjUfoF|tJOMPe3-K2O@bAZi+d$V$Pt@8U{uDpA*-dbd|%o9nmmBQc} z5cFFt2z&CJ`4G!6KqBtqV;rF6kIkt^#(@ce*QLN zs7Zw>>q}_&!d60SZZq=%M)mGo-gA$jj!Fd`4;1>9l(;eC})K3jg{z25qIy|u-A2J|)*0eGhF9a{k{xZytWbVJ#Y zFU+e0Abv<1@5wHXo3)pmD9hoZh14JTZF!d^8IK^qQ50v1O_6O(vos1a(+Th!@rqo9 zFW}(u2t82uBj;!5MdrAvYeQ8yGK4dQgP>d@AShFwqsOaL^|DM)C@5A`4STC=lKYc<(4 zs#@cj(N!*A+_-+3`K%fb3>Q*yu&!Aj<7)#D6E!+-c%p>>$rGg9!sTg~32BVar)G3j zSDG>%03(^=O~oAJ6B(gNVWvQEwH%=gM$z7!j{#K4-X`$G=d5E&a(x5(02wn47)O7F zmU4-rZ}eBJs4{rEZdl|r!WcFl)z(zhm(@p^c=t{i@`!#SjwCkgl14O^T`dl)5%eft zIE?6o80Da>0&=wgEwu>xiwY^|L^}rp4p-;U9y!5+_*8Ld5h{pb*5hUA;7JPR2PK`` z6A#geQTQa-l+2PkHi((M3!_sfDpCtKcLEvF@KNZRkPuV6kP?$2d&U7suC4+OAn2uN z#wEWDck~4n{M-D<-v9tW07*naR6IT`5xmw@SX8Oq3Q5IvA-G`&M246wp%zg&q=t;Q zZD9xk3~D(*r2_*K5;MO&WHBm=Gw10wkWxbg_Nt=ih>lme%oez$wn&_#T#71|Zlm2r z&%P620U7WWQiYXSv5-M|t)vBvO?b&fTOqlqi8sH274f9Z%WMs%lm==5G|7V&a&pw( zE3jBXeZ<~~1ru!vNh+YuR8%J!jH@v6Jj8vtI`N{HL~%rMVlg^WG>4T0jEzWKIrK zH751Ac2xx&C45s23AKzt3rF>dL}I1>mf?&B_EVHKmkqy6z{C<%BJCvs<^`xpGBr;#&YNk*O@< z&)li{PX#h~#{D53{o9wXu!(!C-Qs~PJgB9+(O+L(X6}SF5bV&*Wdd;~t4kNmUA6)p z6v;wS>{ugrli8Q{?A>?t2p4Auy}pB!Qu*HPJNIthG3c0gojNIq)&|4=jq~RZ967pl z@Gu){;|;yRfXx=zjr892TZ2xIv>P*1=YFKb12rbFf`QVSEg1hX zE0N)fU1)d38PWa1;?PHuER>>GR=79Z(2A&W3JBiO7-D*+!09e=E-M7-cs6fPqXcQ% zUX(pW#WeyrV`8Nk=VPL8q920FWxA>bdKc~)%gZV*iJnvN<-KnZx)>TTbx+VoW;@ON;GJj zKs6YA@ObzA$C))|1cP0M{6~6wx*N5B@fWo%we^z^v4PQaVTK-FA;tKhUN)RMv9EVx zcmMolGD;KKk%0N-$>#XPf!@!5u(YK;8Eo(zT|0|xQo;oPbp-Mx7h5a$f?>e4q8|=y>G0j8LeZw(I->_ z4(68h@wll16-FEVrS|Yg9-GWI#;ojuH#vO`O;S7g3CZvh zh{lq!f~B#y?yb8i1RVI&HpPu02%9{U$e4AJj4d9cJKV@TWkm}B(oAjKL}?~L>5e&W zZVWn;&GkWJGJ5(@=U0Ao{F&#=i;I&IPevlWG9adMWl(9>JLCEveWU!3{&oG_d!^EB zos}J4Qw7B#DA0-_0lFB^oU&PiDiS6yf`|~plAZ*VxT!;E z3Y5%LrVl%Smh;Vg>h!cA;}7;7ctU~!>*!0N^%h7H{??I#;0mjW7l$JfC}tcgh@r(# z=2NUeyB-QA38QOGq>4ic~6-~K(brUmG|Dcd-DdN2(E7P>D4&oD6ijJzIpZ9 zww=51oBnXvs8shq@W8E0moJ`qn>h%0;pOBjy8lrenMF?moT&1&y#|lkTf4Qqe*5nE zGiTZ}Gqpw&afh8=uh(HCR|1WT0hs|sO9LTUS*z&j+5>Mx8N3r0;+uNWtZrS{GQV|6 ze}e+~Y0$rS{qn}W70dxt#3P+P&W~v7lHJD)V2MIIE8CX~Ius;se^aC+Cn#86;6?t#{iOP-ghhZS)A={QwWShi=r+nL{^X zsWvY&1@?xQ10;@#Z{ZR7CRqwsn`3ZC<%cxeu*#j7-k$E+UFpi6DT3LJC|)rvCUt{ZAGbMTFa3AV!U;c$pg6p@mXI+V~;QdyF4)F@5_OeVK)-deeH2P(og9MO5- zbMRoRy=6$=vS#E`Q3&uOd4oycgifKjB`C?vOru2_9rXqqch~M-x_CR_=;XM3lx3-9P ziWWXfVeMF$RUr%0QJ-t{@gvGXhO)F%HeT-6f(P=gfF(Zq1D?rlg|}Hw)5a~0d(;SY zycq;a05Dq7cVsqV{6Z(q~#P_Y>7bL{9QOeMO z)~Hijn3??eslj|>qr5@YffqSQ#jKC1*59~P{-dv!zjI?zrzRAYvG4^k)_SAr(n95# zBi$xz&Dc4=$}_6Vo9p$NM)wzAsGm4Ur@xcVzLsFAKI_rf`sFvTl~;RhMy+bDv=HVz z_RYW1i2F+E2@nD0_6ZZ6^ija5j2G`q51#OP_zW^FM#CbC*S3GfhOJ zi%tTFAUK8yp$$pIZwh2q(fyT|V<;#wEzR$=P)gy`kAyro8I8I zit*Z7b^Us`+F5_}(B^;h>CSKc?aD8`Fj|`Ll{ZG-z0aVEK5%u>Ud-WWV&Wi?twV-21>>_pRs1RK@zF6y&m#dYIef}!j=dyoPxmRwL z#tY>Ox3~P}=j*@zTjT%z%e5|7I%++xcDhnUB-qmuIAGsre)`1z!H=C9%`&UNrHnd5 zQJkZm3ycUbh*(auP=&u}Db{L4Ll?WefFoSHkw@4CisJKDrLNrpHQW`;Q9p+_>K_6A(sIw zyY?Ph+_{_eKd8rDYMblpcW&OCaDNFKfTkTNfIxlcIf_&OHJzFumvp2g<^dfK*3~^? ze@WBCHYjJapb&_dhCe3N0uDq~T&orCf{Z|h$+a1O$eWEX6dduiyyA0?9dIyPpu(Xj!$IlU5Y`LSOL+@w;09HW@xjqhQ3BP0cHWb? z^D{@)-s1s?qWDHmY7qedLB3ds7d5~Ir^mwKz_0Y0u@wMGNZ2%?X@Y{%qK?&Fa7LSH zdn!qhMwc}5Xzl75w57gOw4T<%cyglxdM1^pgKa+2R7x1qC^cVw{KL)ZVN$|gG9GLhp^z4Ut0kYhp1cxI$fKELZOdLKxzPglYptRAE&YQ#r4pnZ@voqTFA ziio1fgETlqEEnV`nGF!)R?wOaht$ z4x_K58|tJU-0aWm)Ra)*wEFzSVR_j`D_-0xU!!&5@(@unENk7k4k@(USy^Et9j4`6 zv}EQe?cB3(ao4uVB)7g5vT8wn56eSCh}nn@M(A_@H|)W(R$+~Jv&mcvd9>E3w&{~| z5gc=2Xdh@=fB-07(S@MmdR?nd+Q-fBVX1r&E}8^?5{No<(rv(MPxx(C1b{r?Ze9XP{Ps7|wQE zz&O~(QXF$8p|DUIMg+9)Q~0P=a1d?CRMnW>a?CfX9sjY^6n{%h#qYjRb}iI!4vO!#eSVF7j&c>HP|7?kul~}*Aw$PkBrR_9EOmcLS_zw zG$ch7QEeU=wp66w?VUPO`p{#|IuDJ&n_Yzh`C6qlSzn!e?xpd)JEa>d)w_3iGM0ck zk~ieiS*4xm<89?1et2|vSDjl4G01@4V7K#;C&vHr`^wE)m#3sLU7$erBFZdDfAM_l zUwwUU*+sm7#+0>q#}LH~0^i3x`%rP+VU6ncRKRld+e8Jc0 z$f_m|l#+rB6`i*-H&f+_S-s`TxKmyz4W8KF|Fw_zf9G$P|J%=0o;t>Li1B!{%pC+o zFw`gI!Nz+13$K=c?Vp$a*}rYwT5HeD@vLTK!v$D!7k>j(BL(R7M^VN>d7n;uD86z{ z+r&h?l6#Pfs+-uLC$*(rX>gAPm%#~lGNG|;yRx!euMF0oKiU6{pY8rnpD8`_Aoz~Q z8yK@=Ps7U;Jo{^0+rqfB{->{%{{HVze)Ds!ufDqQ+h5)CjWayZ&Auou&VO55g;dH? zx4bwv{=o6l;vCZUNo>PmnOWx1dUw*@%;H`gduYE6`85Uti!vls6NqUVm0xjrF&Cj; zurS5xWjJXJPBmVf(YziZSmQnckFQX0{U}@RE12hXW&UKQ$T$UnaKt7b!0<`r@&1>> zOQ21_GRRNZI3&wjhB%MWzy` z0yJ{60HT{(>35^i)*UUVFXQ&s1FB&$n6JgJq#DyBF{f}Sw99i-hv8knj83^L%MF@T_ITOQI5Dm1Ytx;uDGp&Fnj#R!Dt4FnolK4@o^mBG*+mUtrE92ezAGsO>@8_-@}*ag zHKrgTg(Er9grV#;T|%*_&k5mj=~L>cI^X2rwHCF|)#=iSd(p3e0By2j2u_yq(g?eB zn%{`QyHcVEpHk9J)VC?V-!sv;`XXU%N1Aj&uaJ7d7V$YNkm7;u?J_DD3W^&qzG@v! znO=w;Ki!^5Bb%wnAzDbS1?YIv4Chg@BEt(8G`lIQG&!&9;ALmL+6NLj4~kils*+FS9ZukfOegI;NVJZ2xVmO7*aDI=UpGxfNFw69OW2k;{s~$ zS^o1a6Q>YY7SfwBB+bGzIG?zon20DaYaf~EzkTWI`h&}}i%U2^QP`fFo!zmmbn$}0 zOTJbqi0Hllzl=H%Cq#*}Btq0SB3Ty-U&vEFPRe8*erq6E+-OxeEh?~nvplmk_`zs# z_s$0&KXv%zBlT9j&jVk$8LQS9_Ip>~eP`v?om!2>3b28I_-ZWBWxA$RUthU<@!VM+ zK~QhZa-Nu{3%KLpfdj`Mdh1&&i~*Rv;R1(FYqHW93XFuxWggci)B|tDfkmtfDZE^B ziGgoo5Q7Cml)!f0jALhjikb_8Q(v(Pfuz>@5up(O<{-ZfPl}@nfk=FCb1Ei{V&q8p zXlJTJPIp5R^fi1Ebci2;G^0SqPf?AIzj2`#QUgEd9a;`tlG}LaTl52ZH>jMH8Sd>` zWYq>jP)W`LD+Iu$bliH~;oeSwLKlj#)QSP4^p#Ft=J9x^x+$A}fobWb^VBG4*Q<{o z>a`m~c8bxVSt5dSGAB2~CZvtGF8jQy)|~cGUe}Mr$hZ>@|^-@z!sWWgF34VDsBPfKYLNG(h*>sqJ)Ow#4ECeRE zV$iVRZ=>oHd)E%`XB>#mIe;fY6D+h&GuULD-oF3jo+MQpUk^oA%>q4FOJZ#bQ-z?0ON<8{y~O~SuCrkE{JFD?j4HJjl_H)^nLMfAZ17l^OK+dqzI*TN(!$WI zNy7keK##vtv%2rtQ6~1^eCaQ5p1(kPtg{MHjL06|qfjWT6FbRjJHRXj#ntl~zwIPob8Jk+xM=#xV+rGV9w(Vq6ibC}U7roZ5z4zYc@;W!~r12pG zqb`N9P236*f$ek7(* zH=_@G8+x;Dfv!dSW#f(#v0Y)O`z>19A0aV(Y`vxu=uTpl`Z2G-ng#f8w*vRWA@Y|K zpv4Jn2VXv*XyNeH!N^IvEr%R=DW08K4&XHUpM4|jfkBV}VI1>gBc`hjSOkqVMj)S2$D%(M_a6s|8A_z+G*!a@3u zh-Weu2!&R3V5WT*RygITP^)(Qg~Bnmkt&wZQ4@s~qXJKd$mj_^93y6w8q=Z~M?#vl zc+i}78SINgOBWZLSQ7ORMwtW!IaLoj9DpKh(ZqZtM(Ng>*F@LO6e@wpbR1v>Y$RzE zf(O+#kw1_o!Qnx;@SD1xgi%;=n%_E8!B`VoD0uKy7w}jP8i**U3Mgr|Qamqhk^)7! za4u079Z5GW@fO_saCmcKg~TsUq3|{(4pHv(_(O;C5NF#&kcrTcRi#KF#w6CU}<2D0&+u49=m(}*0po*xYKiT)fBOF33kS01_@5 zQOu;|T7$4MS-GN`Mcg3BL8Q&acSPZS%($r~DLb`~u@7Bz%eWJ&nkMLo8j!>h3QRDS z{TX5kJWSQR7oGrt;>7{+F*WEk{EtT>pe27mj2}f>#{Ugjq^wP|1;X*W@CIF;Upk&5 zYJS=Pa^YxkroJ!-aheehaYFA?XWg!FP7Ld8C#$9+UF=A_LB~scIx+I|U9p7Fjueu^ zfXiW{Zgp#O^w?pZ7t1gLyEE5{6b?UGBEt;;7w=fhrS#-|7gi~rDoWGAgw9^Pganz4 zP`ro^r(kT1Pd;4##53LcV1vx3-56i6-lV~PEyFLqxp3oFqq?oJzB+jGa_wVJmD{v@ z+@5KF2s+fq@;=;laSP86?tJd$+0H8a*YtnxBc;FkXt~m3k%n7ZSs2XeGnZ?B_)_^# zzTAE4>AATr+}YYg2wYQSjY{d(`ll}Wq7#wWwg?x@^^YPmAQ@&^q9WibRq#H5>DOo9kHO2v)! zi6l@O8TMVrtvH@Zz&q>ZyX(~>^WL5o_k>PdV$AkV{cZKe@uQPp{IUMee{izB$$dd? zax`2=F?bPKsR4g}j9rb>t_|;O%#6mh=F9*dj3tw?=m;ckjV4P2JUuPDGsq>{B9$>D z*A(vK27)xRI*tEP5rgWs{K$>e?G`fVX*9afWZNrmCmL6O^7-CR*M>WmxZ8t=eQ^m4 z+py7lm5po5)fU;|(v{|)yfXM#Un`$}uhpBhsx#x(9G7B9s^dTXX6g98`hWe`O7q;s zz$gro1N$soYei%*+_io3{u8zDyxku6x=)?x{lmX8zqUF1-~DgPH+U@f9Fstxla#3w z!Fiep%z7voRXL1iIFnd_w6X(aPLf5m40fA3C5?(`;SB1{r{^?J@)St{wT(eN(uz2= z>5v#SRqKoQFs&fD)c9A}LQ0P(Z|L6; zr$HPIyS>)dg=5b=z3t#Z<{tET?iZ;yI;+d?z4hkCox9vlW@(d*Y}KCsL=Fu@wMO@P zY4?tYpMIL)7!3#Get*=c@^F}Xv-;M|?3FWb4F^5$L@M$w-AE;-FOw7tu*RpTAUwEF z5mGTZ@Gjl}lG`6lh8EO~QccXKGw?K!bdk6hjy+77Q|sIGz@&Ej`t_UFt|5G#i;hVe z=4aJOAixP4ZZU^m*ipg?BL2y-MZZeZf?NS&@HAq*cO)@_oLG=Shwi|e=F}ifB zH=QT#izS1o7?QyGjRYxTSsqa1*bPjP$Xd#gdm4BU2}KPNWne#&tbd@Q)}t)j1sF`c zw48%(7t-l2P8)Fy03t65hYNfXr%|RZgLvHDv0{i(mXrL2mIjcezGap4uj$Z4Tk&KT zil{e%%Be7z3h^qOwKx>HgwZ)kphs<#6<9ol?c3y@UbPaVj~qB_si2rT>L$t*FhF8f z<)R$rkrPQN14tS~U^!C!3A8X2+HhP_;A;B9P2r-($%&Xw0oB1+#>r`;_2GC2MssMP z{8mM3=TtPK@P~hZQ(;}KU?n*+u8lnsC}hy$qyXc>=qFIN=mw1yt!YM$UVP;cXE+); zFdNy*EXF^Xn|8 z@rj_53Nt8k3tMlka#>qY?3*I2r9pHms^khPB7sc4$sYMpPyXsoHLYe-Tx1BX7^xyC zj^<#swEv-_k39duZToijy2FWkM$(CeU*-D6_uhK-+iNV{;4T6u=PaDHD}HKJP{utf zmCdz{^KZVjxMS!1w(V?aLVuw@8P0E8dia?qH`i9zZrmDin;(J990=Ngf;a#O$i@D# zK3-;a@VNM?Q;NV!`|%HNq5mqI9W zBwxLZXaifCur(*O3vKu$ui~3}FgHB2`yL>pP9dlI7Pf^|dZns)-`}v5e0&KcBfS0l zMi1>q1k<{sY5JV-p_5Z-oW4-LeoxfM{}Uip!erC<8+c(y(ot+BeMgOdc(VX4(Td#U;_|E#vLT5XjF z3@{evA=i(bxVGjp4I(BoG?*gpMR0haF=vT_L=yPv9=1dp6t=0{8W3MeR6Iy9m3kF zO&*rul@e$9Y5n)+A=-AyAu5bUd@oW~R+*{t=_GVYxZ8*j&Fe{=Lsno;^7J#IrM8 zYs2>u0sy^Eiw*-WY*bI~9yCjXXCLeTuYaff(9!jO`hV1~T;sNQz#dKp1Ew`It49AK zCPZq!n2R9-!W2WNWEd#wKk5qD6yuDO3IOEfsakm`*3!+?6tFEBkL0&Uong|>{0p(X z8zUH67qBYmDG=0{xa5n?AiS*#^N1tpRGqeQI=3W(x>2s(^diObaQbAXJ)EFFthZ+# ze&UHE4;@GF-hjt>a8-%Rx5JBP&R%`*JS*1RCW-8c#@g6;iir`JNuGLdvvcnC)9o!= z4>K85ua^dWHn`#<@XoylWwk98B+(J0Wu#FX^n16jUhdvoC8PohcokDg zNCLA0cJ$)j)p&E*o9)C4mXspo;RI(IAQFD&4a_A3I*t_*l-5o(z$ihT;=&$DL-*5M zczWzs7{MgurCQDukwfU`N~3D(S9X|00clZ=LkgqnbaC%{2tFxuPjVXe&SLOGj*^9@ zF2gUdpcyFANkLbq;jA6zqA=rsTw7^5V>e3Dg#^gr00F7uIC!5{1TyODO@Jd?FInN( z_KXk;HYr5bL>u#fT3H^32?kqC%vAKx;R`&_iGyXTyAS~^^?&KBiFD8!W1GbdI~~6; zBPa+6(PWy}82sR*3EbW!6r4Zf7~or1?-4PM>)aSaH|K!(qlEj4(KrMYK_)Xek4rgJ zl@sPw448Q7fLKm_@KY>Tnow4R_#YW?E3;={D+IU0R)jk63kz<=e*=@l0gWa}^Qw?M z5iQ7ajJk@YMo2FXc9XLf0qm^>V_`!^h$$*%1;%^^FB28jf~YjuwGQh+vzWe@flx=L z$R1oY3G|@P3Dv_R(D7`+4ts(6QY^rr@?}-iWYxSW^w0-FHh(QImaiy6^CJM#-KO2cH!do1BV;4ZL!rKKBJFSD&O_v1MN(stTOOF;{=HWYjz(y=9Sa?C@Nq<}7I8IdsKYj`rC(t!bv z$b2aG%y4a&!uuXPdi;IwTiUg=yWviYbPt`3tW|EDJ9qk}Z{E3bjavfo4fY2zYa|^& zPp#msAgW4V=-PYd=eI3B`u^wLCXmNJ(FGco_Z~djd$xP}8!xV{-fhxnE7h~}Tj7jG zxg8zIii|=#iq{T>M)4;-w}6oou$Q-j*kz)i+VDbbK&3aq{xdpt@Q`9ewKokIflQ!M z&vBbgjtngpUGk=C*q9jvIY2)xb4YOn@vH%D}yzh2YJGOVQYo0L~%PctO)a;1OBw33yE`9e(ZF!RiM%ZLTiK#Q)&jms)Ci0^; z5(_gzvS~Zb%4ofRbYJs-p0wJm4}Zkbk^?u7oTW9ynoE&gw>A=0O-lGjcv25o%TQf z;^u4TOQofmn;WCs_l5@-ZT^WyH?>ToLlumG4H~tsn_iWDdYUaD2^v*U!>Kt^ytYUT zlT-|iH0suN9-1r4E~Ff>6D5x)Ydd903qfbuka1?!>mRoUz4q2Ee`a}ga$|M!_Qk3Y^N%_?TDr1iNC49) zeeM+=k5K;f`%63KOO5$ipOd!1v)1qx?)D*Jh7Iw=8lc{2TYK;mKQ#Q(>AAZDo*LL? zRX5?lSi&w3g_TJOba&qE4&j_o)E}{|NhyH_t_mW2F#)Hf#f}hgUD$d-07ey-{^$3zrZORfrKB0BIO=`2z5%OQ4zigF+yPa%!vQqzfE~Xh~1i zSzELU}zrD5d{WbG&bgE6L~tYNU42Ebm?#^M<^gZ7~+OG4FRFY?xvfmPg__4##*C1g00fPWolP0 zy!-C!r+FAmz22trfUMp%VwYcrk!q~jVa}+yRo}S1a`xp{XhgWG%s!iLXffepS9fM% z{^a|f;p)(Pr{7>@DHqVVB?zRP>=0)AOekZZfLGdZdIAcsVqhC@4=)i!rzmQtoLW#2 zSdP0!Y%t+=mh!f}d*-)n8TESH4a^?L{f$-bDrPfKgwzs3PozslFE;(zP4ky_OmERV z;Sr7@gM&IPOce#!N5$SO0w@Tfv>BnKQpy+7>G6<_xeAWLrE@8Vt1clBd-|?*QQLlF zkg;viFZ@e#LIgnbo(}y{uh7L5TsqS{7$1p4z?XmIFVS_pAHj7=6{Dg6>RiYG2_@sV zj)jv^1R*4lhDB50)%cahvM7I26&x6tR*Mdn{%t+`Q^*Q`C6b-=k%C$xrUY5$ldv&k zasb*kYhIHEbH!%FOUcM+e%6bjU~e5h+Fw@mHt|T>C^?f*jO5^*9wF#Z$ed++U`D4h z3gQQ9%v4MIBMKIs)zz>5BCjNygM;5_0~$M?!1di`Rf&~G1ECT)febGV9=jA@IYv1X zM2@2il&op@S1~<|*mxo%Fn)xv9$;$JM|m&aKsbgGD)pfrAc&cMN(H4B!BpW`zVW89 zpeU?z=Y_vwm9V3t2T+f1VMG%YIXn#?XiL*VFb!GJ@wgnpfm1?>_rj7+kN#jtz(V5u zvUW^t%^b3#Va^1}Rs7n`F3eIOn}bxG+Eg}%~Mo8o9pA5mis zkMqmSLQ7Q0kjO)5_gg6qcOO3V*atqav~O>p!2nF?;;`4kcyjg3nbY5T>E4waZ2iVP zT}d0dCH0sW(-^UO(Ym0KD}06no?y_Po!_>5C&)6-00!jrg{4J?PRloL_Bxx{#|?u~ z!=8*B@hmMy;?Y863}i-9Yzu1q*F>5T*RM z*zx3`!%@?3RMG!cQjw%xdDejWq9gt<-o4;r=RKZFyIN#xz*1pBtubA=rKDuuBq`t0PFKQdy0`Tg|BHGpzuwt97?^xOZzD-86{*anab zpWV*j)B_bZBjTn@The%;)aB)LYrQhQb+`7&-tjNJFqtiH;-b1QeL+`nk4CN8|NEuN zKmFs$%CO0*bOx{=da(E4zDcD={}p_rx46e`>gDTevw!%t;ksMVY9xV)m6M+fwNy#= z_gw@1gjA={U`zu)@nH2wA00K>mj*OeN;kDc4d#j35 z=l+4L1wwlz^ca#Q_E6TQ7L!L(0IIP4E)DSTqo*dP4$$4AFhcS|0(@=2@bT_i>o@=X zo5fwx6!9L`Pdk$fG6Cf)@%B|Yulh2MHKiID@-wb=RJlPTD zSs&Fb&oE`c+(Yedr~T5q)!+SM{kQ&I?f>~!?abxw=BVA+Ql6iU9}!D3hO#NdK|_KoC*M6@&qR-kKU(5e!QV;~JCAXYMl;!w!|;)ZF8-~^CRwrS!7J}Ob@Nr)B@ zxFHMPj?FX*GIEglW3WNaE@a4^9ts$gg{0fiY!Yrkz&RM>yJO=M|o3nGr zPMtdT#HrToybpD;-nh}2S-pGbjhDW8`_jcK`@HdZc%e}qf^K>>L69JZ&Iro2W1btc zvVQwEgS)x;Ev=auIS$!6Xk_cKeH-PL@7)`9d9ElA4gPOF?jLmcQ8!5?{9?B>JX%CloDelSGoH=JddSxO7JbzlA!Pv z%#L@c5#;#IX$n9i|EJWoI0D1NHc|?4dPIS?7ekalg?LYsEteFGxSUX%=<6B3`xar% z`qn~Th2Vt$(Tslz?jJ)y-=QErCSC;8m#N4Jh84vfb^Z)}TOD(RTG%Y5gKI_mG?Wom zzk6vRLPLdr#d+6mI`5q|AB7bFgvC-cU0ktPX8iOiR&?aE$T2agG`Eo7p$7y>|1e-R!W zN7Uj>N)QjVzeUX&tAIM!FP=a2@X^_&9rTmvv+X%@E;gJZ7m_>rq zNi>+qil?TK6WS4rC=9%|*cmKyHr#Z1G(}yOXAXDl+I`~bA7a=4E>Cf1I)^>mYo&EI zdOLUay*J-lxp$9;6>vR(DM5JX+FT>XkJVLo)hvNcw7nm%zOlM?=GE6`7v^^#J_0|U z+1_JiZoBo+qmNJey|-WaPJgq@er`PEkq%VQW+bjpiM@Fb$Pf<1kPzLp646Fhi^cU9 zPGA;V8E#?Kc-D1hwt$B^+RRx>Fz0(vvbO~NlrBg_bciFiHX)-)S!({_9^xct1dDW{ zT9glZ)KPgpA_1@)Pk4iA(BQZK1yh!cg#y$qY*<3rmkcw^9r3V)jXwG?fsI(CBo-5@ zwt0ocs1O)dX4}K{6FYFr-!WAnw^8O=cRU2lg`E%3Vyw z^Jp!LWTn>R`px?JTdY1pWG^$(AtNwEpitUR{$uR4G|HF%HhEsx9`z<$7X}}EWU@Fr z8L#1^ta8Gj+!ez7^W-mHue^A+&Q2^|0buW;Ms01Q`^H-X`^QVKW%kB08Gwc18PU-qujc^ex@JyUK z)c`JZvQ4vbcQnADW?W+sRGcKkoG6_sD^8=M|9Mj!?2O`!j`6xo+^V;#7d5;}2N!6J zXFm0GcVS^P$D~t_g-9eOm<-u41%dQFZfHaa1&seq>U2!u!Dwc}+7qr$^qF*W-f5;$ z`N$)a#|{iXaAN%1Uu?hldZjyVHQI31HMv6?N6;|Fxl@47z-e{Tkdmt#P+{PKS;iRy zIUA<&Je*~`FjwBaRC)VS`wOSH{Q380X6u8;_VxbmPjK@Fk?8&d+8yU4XV5KUs<=De zIzRfgPmIr9s(=2K$#5W9EhXdPVo}ylXru5!geYg=Aqp;O_~>iC@(GI|>AO8$e)6P< z8k-I13p+rv7uigCbKLm-ua%xTHvaVUGquU4iJ$yoI`G!nMrE{X3m3zN*RIw6uNPZ4 z?+)t=W`5U00fqTr6t$i$Cz$7#$xzKOx1OX;_?2VK22-(!_GB(B;7c<(oxmq_bs!Gv z7!WXF5PfEZ;*V1Hq5wmD#FL5~lZ@YCzz~l+##9>Hb6mM3i8WG;kPu`&S=I_EHJdhb z2*>#15d)d+dR+N){HdoOdg2Kl$=vPtbu6!;HHRDP@1FkN^>;7Ov*#gNY>gD3LJLHK z3~K2!n%h<$-{5Xx)=Ti{m|Gj)dg<=}*{q^j6m-XIZVVqKOP^~U344a>%>LLB2+%BXAHAYrU zlC#BMuxX5Jc}+L51LDK05L;{D0Gge%UfmZq`yma?$!6WonGPi9h%j@Urz$Zr(M7r9C|4?>1 zf`V8qZU;PR6%7-j0e*IYkQ>S(QP7GK0+1NulfM8W7YkYvuv`jiOi9O@M5x>{I|0xJ zi_KK7R$4I4q$McBDZPVVv1(+Iz2!rIQfd`}1r)oX8M9-W*?P*0$&@p#xpAh}r~~yv z7m|Yu{j-M!SA(fzoEa=!sX!o`9m?AJ2Hyx5sW|Sf1W{WcBaN70Mz*725C9F;Rs^b< zBjS$=L}BGqG+)t^ukjKT(rAi-OK9N8gi~N5jfkQ?@R1_YR3{GJrzJcx)OD>ScQ5z{o68AAwzQRUY4yoC0h6qND zcn}q=L7tiJViXJ=p-?*Rq8+mz6I22ST=X_^V+9D}b3g~;SULweZDR2yAuFP1u<_>< z6ui&K!F2H9qMUt&bcrC3tLT?ThvO{e#ShX^WT*l!z0ac}h82lwZ9%NktAgD098Bem zw<;py^TE7gwX%Bm_Ki!|j&9q|8cXh2V)L{8$BwPsx_$k^g?_hJ=V4J0QnXwOEc8pe zqxp@D0t2BJBt9^=2l=ofhy5v-@MnH!*q)nv@YIvL4j=BpZ#bH5G37Y9{?5fSue@^e z!UcyR_2Q-`#MgE3EhyU20Er?`sx3N$a-e68ZoP5u+O;=d{&ut7THL;yu{TcK?Q~nM z1{*?GT8*>cd1Z5X)q6lItSKOD#SKsmZ;EOkaYt@+jU)3NxIO9t58FCB(W6$AQ9sDL z1+XUPA7W~2Do}~?IxfUrI!mi3d<`j@AL4N=;f$3(qVX67{2z&J&mPJbJjAN z{Yh_uFB+E9$Vp$opu@`BLnctH zTv@GMzN-UenHi^(fD;nMwN=jes{o0>N*qGlYypAs=+uLgkDlm(1RXgC`n1}Eer2wD zZ>{>LFE_8PR$4PS3`w)pXijh<8M8DPgC^8alOq_$B1Oa*a=Kb&P~AS?;;vJk>OHQu zDs61Xk_vX6$97Pn*nGX*R z?=Ju2KdOEHl}d*R@Fp#uV&I0tq=(sI&}7KKDHI#C(um`jfEz84y->#lMB8o_AM=2> zQmZlCwPWVo4Q4}|8>89vjq#1|ZgwW!{Y&Gg9;{84-5ruwk@4yKmWS2h#^mtM&foj- zxl6ZLDpqaHGZPW|`o94YfZ}n;hlksqDUe>zIVsvWQPwFuz(mgb>QIg&z|BICHmk`@ zG8}+_36{HJc=Fvp_&2qe&Nqg=+R9pMo!crZefEUpb|n0d!aO(yfULyrDaUS(2)Bx90vkEF*2H|{J*bOzx?vB#152ou9y+g zwx-bHgZ~H_Y1&Hw|mT{Kdd-C zcIxp5Pd-*|GzUBpkm(5Gnaw=QlXuR({q7rYur(CR_gx)L=y(ZM^ZL>$`P23`I-^;W+-xOC?G%eQYGIC0|eiR1H&i%c|a^gFXN?PE_r)!A6R`2Fv59Sl2B$5AcR z(xZ+!X}i-Y;;8t;%Dj!*7E+m@40EK2j`l&^(}GsHwD-`#qoavS1*nUacA7W{F&M;1D3hU~W{R(fa+q^ktwRz~U?MRaPED>L%8zfG ziOd;JJM$(|p34{eNRU=&rQ?KMHw=I}Q-w%)dM`JF=&}F+KmbWZK~&|_m-jgGB8~Z2 z37qSJh;n(172{z}%TQ%tCKYvZzz{u)t}&&JNn|rx3zAAjDco2SLYmNpGkon;kZmG4 zsLO>ju1tp`HW4|`CIGZa;SwAY3hj^vI^#--;6}oP4(cr&0%=*BEdh91cp?gXiEAP9ds(Hr@0ar-NX8LnDXfAp-_nGd%>4)e2EwesrC3V z9og_zC$^0A_|tt-PSSGmqX|VN+LxNAV^{%_7}jrFL%GUpU?NabbDXm zF6TVYZ~6X~XF1P#j!w@%NK=Tn;(r1b3wy~CgRc)o;I1cJ$O%det|_2z3m;=fSL$! zh`13d8SQwsU<;y5jI6Ys@OPZ<=>lhpgdqhH({sK|E`shc=)_7|E{&lWXnFI12toie zXo*6ZROU?a+l~%pBqDAUQ4$-#XdEtz$>BZjUfJ7|Pwz;Oa4sdFnO-Qglrp0ErNkpM z(R%i&$9j((rJbOKBeDfjmz5J*{^=G^iw}Hajzb2S)f0lkN({YjsN|twUym+r-MPw?_5b53|u4g zv4<-U9~-gfqoWl)(p%{?v#6oD2y;s?XrrtB8bn=aeCJ&2zxg~56DYU#as7z3Ie=Gv z_SE=?A0D(=|Lv{Z1?&ka=CkJ}YdiCQ_`+mux61Gt6y18RC_}~|cC*AF`A6h52MTLA zfdxZnqz1z$A1wXEGb6Udne`c7(bHya1f4LZ2CBP*!OiU&_xZB$U^u93^vZX3#&2J% zoW4+b^}_7?S8A`lSAG3T^^L3bvo|W2?pAqTaAuJnhrO zRM?yHiTkSW+#FuK!d6^7$U`SZB>gxJ9xc*^!$I%>&au80+fFARd2sgpBWw@DcN_wP zSW};?vS6w5wbx2-Un$kv)fRVb@cgkx{o1V=d*46*SaW_3{x)ycq8cMYEHYusozbyF z?$LDmLTPiS*=obg$G?UvEcDSNKOw^t<)r}ulC&$T=;wbA;y?;S1=5seWecZ1k9jWt z!bis^4-UD<1xTQX6qRn}t*gy{@+aME%e8G5d4YJV+U)SeVAiU+#>vJlQHbA!G}J}H z4~`g=ssq$?0GDr%_svZ{@-WNUNs7#+lBd{$86&eov2^Zgi^np&eXU&Ww!HK~S4dZg zd`5*Y-uZ{#MO>j?aX3LyL|_4&3I@QUB5=}Iv4gC_$O@gvCw1%L%k&qEtYrWolRcl* zHuq2qQ-*nvL~Go@LU$T@wnOd%B2fuPoLi9QgD-fm*k9DWs8*{&6|de%F0@NRU*vDi&g}M z`rA9J%eVVGTiyA&`K3h=?vvo zR$h8A^6R(a!aI`iRp|v2;Gmt;5sh?3^{5R?R6$%Nv1|;_8$ZQHYil^5w>+|HjKD_C zkd>9?or@tfoFtG;F?oX!01G#%6%N9uhNK@Asw906?4>MANf*=ORz(55K(+Stv0sQR z_J%^2!jhB`F-h69SLPxP7^~1D9Q0n z?>1k`#ThkCVJM@G5ycUk(a@m~<4w3|QP@kAn$$v5>p?xH_PEM|9kvoDY-vBxdnb$@ zd=@Lp44rJJGk8P5$S1U-&j*#LK}u4L+}dp40_5}=$}66_s-fPJ@r9?T9#s+%=&hWT zhj$^ir9}`@dMvMi4>I_gn8Kc99m2}4dqV<-Xp}Ai`IGKSM5B~-5|*TD5i0!0N->DS z!d+*QFBBQvrCpJ;8Yvh<2k((~H$1UZSPG+4qQoQghb&E|aLlE{k&Y63nwMAPiN2PG zxxu1!;8%E2z_DHQw|NsRtw#pR2NG)?e2#X0?Z(XuXU~1`xqU3P9IOdZ*Q-XL$b2GfdC3Cn+fscNC%`UgJ}65+(4& zIGG2*o}xw>Mu?bC<1^>U7y!PbYj3~ZnO}PJBOhvZ+r9oS$P9XYt^(ix$YVlU=`*A8ugq5K;CS9Z$zuh!n;i{KSh) zAF)PZl(?5Cu)8C00FN2*01Ynzi6cKjVhlzpYU%F8l_}0V9(%>3C?OjS3L_m+_lO@- zhm!u<8$iH8WD3XhoNA~X;+U$L>8vsr`^L5MPdz(hgKTd7^oB6##FZlvc(eIo>8T^5 z`}Q}^E>BB5(FquwffozAwdAd0J83-HAlD@njCQ;HjbrR%FK>$;G@k7e3*ts2~Z+v^to!e z*<0D@{O-4>7jM*S3-l?{LvpBDS=r)(Lv7aZIj&ArBTXXTX53I6HhC#Iizy!6lfXsmyMz zPn{b6qrcm@yfS&`>Y%p2K^MUrYyk?(*>xe6Xbga|!NB{CP2?=E^|^jHY_~Na`f5zN z)m-mq^QFpcu?;598>??=)NVGnMy)@7q58p-{a^oSh6AicBoRRwbL}yl3>%HP!N2?I z^2=8ypZoe|uh*UHu&2I@0-djf1I4jj5j92H&<_4qD}mN+ltn<5IcNDL(&$36!SlId zrA@d{<;c^mt8TANdXsvIhY-x`aXLJ!!LU|h2j{lgwhsn9|Ksp*;0r9`q8t(3W@o&; zQTyC?N*{k@_~~c3FWx(>Ow!bDmbb_4-}-v>m3OBVo;THWRMzmEo)M2mW|?brq%WE# z<3|^QJdx-Z5g-XFNFpJgnBx>NcLA3DSxSIIoK57^nM5vq8Ynv}ywMvLIga77sHf)& zrk5-rC2t(S`*uZ4Jjqjb5|F_z6oYrvR{;({i}n-(3aa~Xkm+#tdczMEj~u-B#K{LA zd368b!=&yZ7v(5fn)3KaT8i86T|NKuD_gf$t2|u7SR~08)Xet^Zf^;SCz`6V_WAR}$%nT68ab&5f)Fcg2iqc6>2t+-yuKoq&_x0T!-9J_Ls*hU0U!_r z4`@{jOVeJTjdXC4_M2}5Y~=?)Q}2_I2WUconFlm<3;Djd^nuF4aHLa zbRJlN2rOZYKRLkQg_V&^)c67ii9msaNX$va@GOOtnNJvU+&0L9Ol1`b5Y#x3>y+@Y zvoB?2u>S=G4it)U+@qr7{E-cXZi49$0X5Vr>=R<_qDAikay^7Rb>XKw~)4T~0kS zyc#QVupmw0`Pkob@Usn(Jg4I(`h?}x+?Sm+MG#U=E*8F-8{lZx+p~%Mm0CU=V_+35 z;s?|LnoQo|MHE4K!qeG1aj=#xr!reR3P%he$&t8XE*R-CB7t~(Whf*gDK`NKeMiNloMz8B^cNN|{OwP#nd`)T)+>(n@a;VbE-5O|7Tv`wPAtVM)wrGPv~S z*@Xl9Pdxf~tJxe3`osP%PxZb3)I&TOU}gE%*6QlYty|trySvSPZ9L!!Rwf+aa=pl` z)oLx=d-%}($M+vRNIz`c8xD4Mcl*0MZ+LZOb$5Mz+#lgx`wrjx_=i8VcyQl<8w_ah zr=?48zH#pKtJ^E9%@)0XdK6#{adCzTx)>H502Q<`v~`PUh({z2OfI@PdNX-6>JKiR zeS>=pAAaJgPOIA=u&2pn%yo=rW8u)@Q;Uphj^Dm=dHL$~&AY3^?M+sb^MIIY-Np}G z;DOUK{=xvV$7D(r3n>)gU{Kg)LZrxMC#Xo}91#!U7xBND&yy;l40#V?guVIvf-L1x zQzZ>_KMUuIPlg!2=Mo#0j1w3jx~%2-q=7Y}IVmLimd!BClwmK0$6KId1E#p_P*bb* zN24>B8{0iLTOkVRRWUX-H9HVCbEfr}P9ML2`rIR<^H(dwUbEe?_f*jN(%G~QY<%$b?nK^xn)GY2AjcV1+UWqII?8rwBrY5{}WmOhK zVHAcO;+Mlh#v+K#MJ8sMd!KrI`sDr2zN@YmD^j6VzInU)%`=UiLAkxa?Y`tBws_$G zgxR;=cKNj{)s5ZK!PdC63tNl0g_XY9%f-ih);?u;~UW%?QUIF54 z`ZFBg*(mYg;7XT0glt@?9Lwt#5N>i;vHKns=D>P57fBKijomnbR2T#36eEt2!bMUm zU|;|$LkwxBU`z!sI|@)~+-$#cssAT`RcY;O@w4A&ZyEZ{_KMhcUeo)C0{&6nq3!kC zrL&jEUwy62=;$B(TeXjz95i?qA$l{IaJQl=8$mI>J?=3oo<0A;)-Qji`rrM*aMY_a zoVRP|6Yn6V$k>T*ZoHEohW_NW_wwEC%7}|0aJ11U$HN}4^a0GZ+VH>vS1_hy_DA9R z3TbCFYPBo9jpo1nQvbP!D$jku$L?}n0Me%C@?bh^YPQ~tU4wr8Cui?mt-butsOK#_ z>FuJM{z*yx>8l*T*HNfAn3f(uJ#A6s;XB?6+_1N>?#8}9jZ6Vpk$|^r$`jUVZ!cGN z2i5v~mHQ8D@+eK>rT=BsJrl{)jAJM|OT-DoPC8iE27(oJ)ls9{c;~&@U%XcP=#%|s zv)vz#+m&IZJsH;8pZiMf_x=s{JJja9<3j5}-w1{Xl;QRNOaF+VZwU?vxf{Knrq}C| z+CP$+KNj8=!C;*-sIsJk)O121!VG2@et>j zpcKObR{4dZbgAYXfxTRQkSvBwsuPH2!AfhPbM*d`C!c!kz|rHCdZRxYU}yy?&6+;O zw|?i^6|M$z9~u`2ZE)>`8b=!31WSU(52cBm~!wk$xl|WVWDppyGee#9YPdsX0&0{Xs%Si%; zpaWaoLx&(H<$_l^a8>l!(DnXYzKxXp(bC(Hp@~9J2F`A>fGf(0kd9GAzEC*jphRioQdVg% z(r~2(Qdmu)V&)vk0-we5Lsm*DC;#joNF zS4Ku@?jx??NgzsF0PX=#Ux=}V*(qoRvwaCgs-X~_8pwk$rxHY*6Z2Tjutyk8PFY+A z{`n>30EwjqQo11{H@y}u{O?hz3VysELY%fACCQ-1X+Gh?f7tq_$OVeYt$TbSCUcRO z2W3JYAz`BAM|^d<5K1F}TQ>diI3$0RH5qR0UE7hV2Pz zdr*(#y-*zZQ$9E;jg5ge_q0|>0&tz4O$&t`+4x1`0Kh+N76**<5EBWdu&}M`-xRct zprZ)3LHNR6!m1T{`OV_Y&6pX;T!d*jVPcMZZ!PNqYC9Vn=U;uL)oLC+ak9xqiY$~F z4w;N;FD~4_xWsbJ-L0*yyLY!%?ryEGZLe+gwzhgZ+i+>mFRO_c)$&_ zK~nzA3rG@?_9fn>&lgt<=eY!>r4ezd0L|rCIwJ2wdO*Xh@>~b&m4obNEJ3Z z0%;LS7!Q8;O`Jhl58b0@$VT+R$zM(_xrEz7^cZ8D3*%~w2)!Lc&dT(4V|JW1eM#O-y>LC($7V!)t zwlf)&+U@$k^YPkC?+njf1EzMfK|Rpd3{;@wNG}3AI8Rn7vtqu#IVyQ z?^B?tPK)Qw*pPIn6$Kw|T% zd8q_9TD)4PdiHAbcfVO(*(!CrbhXS8UEMD89H-L4E8Hlo$d&GkI z?eQ}wr~kpvcP{?R;q8q|ZK=tV+H@gjYmfoI19Bik19;N^uptH5tU*RA4I|bOCg}sY zH8JketZ^+uzv86l4K4P?tz20EX+W00uQF5JE%Bgb+=%7?npxnkb)B#>eB}-ig8)PN z?r?|BvMn0tpvI=O^#)I$9)JDp>{o8qPaP*NxKq7aZZ!V-&D#I?XM?rDVw2k&D$E!o zyv7bQ!yu8OlM=HTQwe4Qgd?&y=;f&U&q$FJo$%x5m_`5;Y8_VJce_gm58V6U zgZJKd|J>37SFV^Kiq#wwHo5#;n%#Nt@*6LmUcUOCuEtYHivA>GD5?AbzZel@@&jc! z>IDuvOMoKVc&TSdPP`ZcFua^zedj`>)p_!{=jwBv@nG7XpMU(BXXaUdHmfY&bD*;@ z$0D2#Ks4jei(w{QRa zzI}5``(Qs*S?^fyG+V=A@4}mJojv`^_R31V-NHX2FwKhw5Xxj_bGklisuDtS5SHPP zNl??NfX6ur+E(*$4yC9)dwy}+*>})T?X*ZxC5;!jPzF5SA3^8=pyolS@t%5d4j4dy ze-M<8Ls>d-5zqw^L^h`_WGU!KLRG9mDd|en9B0N#s*oIwM8U@TR;HHp`OZ6jNnO6s zVN*N*_GF{olV-&7l+mSQ`^0SfGIFM7p_ASUt)j>6xRnavZX-j(Enk(zhlMfi7Py&l zk(AYyh0`RaF@w!Lby7v25?GEzWDfG%fq+9T59S0^W1bk3D&!`C)TY53V!@koAtUkN zf!%e1P=O8NnPO!sEQL<`1R9yExmN1TnE?&#*d-HWV(AddprJ+diauOH#~wy3WdX6$ zoggNju>|M&qb%6Tl)_XMU%p#JapRnTZB26mq?nlFx~aE8Du9;|ZBg|C)g}@g1nj{e zKUkQKqNeLN0F7g0UE=~fOPKRcDe{ls@e4jRSB|5ODOT>d8y($3bIt>XMv3TQDWpjT zCI)OSaPng%CJ3^72IhpV9>W_I)A8pJZ7n=d0&-MPJG7xxh!^4`oz}HXO7R6E3hYPl zMHVcgQ7r4JlVCayE4CL;aB-0rNmWN*fsVJOZqDX}mQ)Q~KzNbCYN}EQV;p)A zxp;}VUyY2ExACAbRlrV>qZ`Y1s%tlHpZ@Y+KJeJphd%hk{DFh)8O|Ik^ZnSfTB$EB z?pxfqRC?&dsK-tRTp`$DNH7?U7nk-e?%&^>W74xkzl0QtJZ$id2&Cg?v(>rx=)t4+ z+Mc;u*f|1Rgv$$?cW%G?=IeC6S+CC#M!`{qcuez3yKC!PIGlk;q@gyZ^-4Y3BfPFA zgoY|y`RC5;pi_eb@YGDU1bypI>B$pB1FQxs@mbkawo!e89zM~-w)*vjz2a`g|sGydVn7v}0)<35+Z*_kITdDbbISeO>= z@BYXK2fy~2#()1uljXJYkR{RW7L)gg$R?1|;sHi@%Z!^w2Bn=IQ}myCcKBcX^!5`E z*2{hG0QT}CX~_^8Ppj?q&GGFml;oNZlMr}3A;Hx)lu0P67kD`4M&Jc9cRPZ zLnrG`KC}yOZrJ7WJkCI0u38`TdhNe{cXoHZRG$Yewz*-LVoGC$Q61{_+0EPaOUtDn zKE;n6Ac266V;fqx=N6PIZnQrx&DGf0{?hUr+BiK1M z-3q$N2BScf%SjWqm_!J$Mmjx7^Yg>rHeaxo7NVjF(BO;@nXI8hFBAhhOxEaTJ632V zz}-*@FXYkPy~=#4vcswo-MQ}Vj9g-49SeGxVhsAk3h@#ExdEWuZuaYw*DjQP>#L(v z_s>prS$n~i0@XBCgk)4h^ZCs$&GYW@gP~U(Qb@eM6ZYwdF;?4&Xp-*w@NK` zkYoW!lW+u*O4Vsy?P-SQwhV?|jt%gM1Qr-;C04T-gvwJye5q9$RXeS-*JpoxdiLvw z$(BstwO)R=`k(#I@a>!XItRGo!lHA_$H8oX&4Y?(uZOQrM@gIW(#@y7T4ba&;d-aB zMY@bv!&^{@BfgnPJ(>oPS~kS|E95j$0Js0tL_Rt*S%fOVL&|QbKpoQ<=9HNUujoSP z!<6So+8_O$FFLxL#&BJO2jU((`S8L$`|&~&-mW(UVI~LYI4~EDci%gA z?yc9pyS{vvnRQ}Ct%OpK)1z$bQQKb%0>=a&$MGq=PzebJHXYzdF>*i_sFb<`yjOEwzMEiFCv%!g?Uxa&*?beNedVp@qsur}d1b>rx<`wriGq_?xx z+uGUf^|*BbRCvNbXTHTVbK31T1(^(DhXn2psrjH{J7nyuap}w%E^cqGt#d^k;7}RB zL6P?YZzNa=yYiXij5-rJ61h5n zFXlGQh^A2~q{3RwfWUt!<8hVpGCcOhS!3*ixyKa}%}H~7O|Op5l)Cva33X6bRmoSm zTi1Sl0i}AGw}TAyRnP9Bxe9{ho5&f+<6vrOAKEYna7v}0k?;ns0(6a|dX}|}Q5AIr zNvZ&uZj3yT4>K4+8p?9EJ@Qfb2&R?ZAHcfM&K`l?U5dd&T_DS!>5OBlLt{ceBbL_9MN_5koGDNf)ILD?yTJ1-gxW# z-&p(Fxjw+62q=-FkQc@b zEpUH$@i-_S;zi=)c0P)*`5GI%u>$zcopZ0eeEY&>EqXE6e`WZ&h~}xzqWm6{p8b69DU$qYi^F=Om=BQQmD8= zOs(Cym*ty>?_Jt|=&e^@-B?~OjHay-0Zx3FBWPkwqrn>2#A0+k?72AsoLeFiQi_*@ z!0TqA#)Wp&m0{_dAjK+L27N#a8)98J$qjF?1|_wvR+6$cbw(`x#*tYwksRAelat@@a|Z0SjPO-pUi zjv+cLiiSV&WbI#{p1t}mK?7%m*G-|U!0!u-OzKO#RVJ&w(l!rgFg+tMJx1x3rXy2l zZMuJf*;SS*A(BKK#xTRrW=nIm?YrgAeSP?m`>LOKeBzconhhQGyD+Ry$B1424}Ye9 z@$T-QzS`YoH(wWlkh|UN1Ssw3CISSEfuxsNnmo%5NsNp{A5D|-tR0mf>VyT4h~OVu z3l~EO4zX-tRvU2xz)pWy>iMt?Rw6lOC|6qS07~~AyN+wNoN9&za4)!P1RlUKKtqV+(WIGPLKZ4Zx3F%)M?J+9OPDzz_Lc1aut~#bKCK- z+gKo`x2g^%C8;32QLTFV4}ze%iU~{fv-^i(c~?x%70szBh5kHzET*4obBS{tJ#=m% zvo~se38-oKQHw^vo{7exPHe|!%z>u4eqT56)pxff%dq4Qxb4MO{5NK#97&Nl7 zp88s$%6&3*Zj&7jdMh_BzIXmSt7ZCIJ1oZ}<~g5bNU(=EVivtpTTJ+R8qmNR5iqwJ zBCtvrDGEjmDHbXzJt*>VuMd4 z;2~mUjz)K`IlssxKU&Mny9V^Y{)BO?Lt<3%;oQ{{Pqe&w>Ee~MXYXFU!B)GiM#m^1 z6!zw~5DEX+&Zad421U*TX(ph>98W`coqzc7WP7I?jHSJGu zfZ7`0$5f_QLLHo<%2vsYc_{@<$dRbdc+wyz{o8p4LhTC?p_e?raBA%kj+}_795yvT zT9*VEvIug=Ln52pQzm!nn8?@s?DQ8Y-S}rSVQ! zQWApttcVtUIE`d98;W>K}b=|y84)|OeGCN4f7C3 zyrWkmQCiz7og$eJIHn?nDslh^XV}tL5KUUTM7bl2Afei9n#gxU2*RnXf7whikzyt2 z`s6*CDu;xf?BF})@l&y+5ZED0s^&n-DxEG8_ko+L3M#lI-El4~IAijVI@}Chfl5); z2tZ_3_Y3hSd?63%6sKCU0VMKFHP|ziqHpK}W@JI3sg3=_MJRmpu%H+ye&si`6%Z{2 z+%EZ^Pjg!HBbNU`yO@~J?Bb_PY9lOSRe)kUNc||Sk?>7am5hfpw}=Q&6f@pzuT@Sm zT0v1_Jm}krhoN>s?vSObc~Kq7i%ERCBPU^`nuL-)8yDRNF$+34wX_g8-hp9-4b-@u z_twRW+pBALZrr%{f#W>Qq%}9sg?suCxr>*F_`QfzI170?au9oZXA2$p+iURWA4T$fPycBVm5B$nUgF=YSte( zb!v0v&c@wk=6JXYY{nst+Pv7Jh@$al2M)H(F;=JSuxP(&&wdI^S3yTm0Tf0G{siPqe;JyLGqrdtV!W=wxGQt}S9Mk3$UKp71qLNUE!5^^ekoeW93#Ft0G1= z8qq_y@_<84Mm5-9p7e8Yph(9KeqqTW6RvJft;#gUM-3OSM}jzlc!Er zzxNK$#4Xob&QnBs+!5&^=s*Zn;m*}COv%H5*ZWZ6P7ftQnn;Bdy7xkhdm^=|!g-b= zTymH&0Ibi~&R&}R{C0zBNK<7EYPaI zaiQ_~Z`Yr=R{7`uYWmW-W@D+s4hTc=M%;j8e;3f=j?gn|`Hj%`WAfk(uZpoCSG!8A-2 zWKxP^W{jiBGzJjIv@fF2MpWwc`yW1e?}-yE0vmFziT(^D0KjIzSZ?xYFIwug<(rq! zzqNev^2)8-q*xje?zuH+ZIHwmp`=IUD4|H{=F6u!f8^97T%8`k#R~w~2qUSI($i;f69e=z%DC^G2U;_I6(~6^UH%LJ{%9>+ zV^|(;@7%t6>FR}xcdlRG<%xPeqQ1!-2L&k&DFjj#>iNk(SOqPIW)PF6FJMLheM^Nh zki$nk7vsn~`YS1=4HTOw@-;t-)AV9Rh z0p@rD8gfdp5l@$qxNeA6I6m-&CcG%(aazNunsiabiiY4MYFW(k6fsilBBUngaEiDP z_0$lu)PlOzL5)OBP3!5? z$|HwRFiBG?2%3?}WGSs6$_?yP0~jRqh<#3|6rIDw6NR7@tEn=AF~T>VJRYqQ>UJ*G zhQ1sxGIUBBzE-BF@**7GqY4~l{Q?ltzv3K{86nYIYkI5@ONa6>rUBwYJG4PvT#Ey^ zJx3|6Iv5~SP}ojrL?I9~(_U1eHZs%Y2*RMF`Wgi_by8rkFnA!3lB)~_3@4ZA#HTDw zO-=#2ypc`;M6lqCU<#!7*bju9;TSS;1@mh>MwpUuBbL>?f@Tfe)g}-kLRyUc5D1GC zVP+{qgcst%86kbw4?!HKunDv~p;{cVAh_$oQ5tkq4IuzSz!BY%h1$k+oEB^qkPL61d17f--UbAkq>6};T2^We(r*4p~c_r8Dq!i7Wk9Y1{R==`C3 z78jRltv1PyyoXnj6v@w~KQy%Vc+~H0Z*6aFth?Pli+sqh-7b%KM!r=Ik%Bg+R?z|dLw^YrEY|f+N9soKyR~go}*cuWU zk)7)hTtC33*g~Dt?ps>ORj>F09(6jflgZ%df#xIku!Mt6Fh_(}656EHsH|_#zIk@C z)+;v_a3SisBPEe<%E$~#v&y=Y(K|QFch+Y2AK=mjo|LOh^gFO$ydNRzO>WdIeg1{g zOXupdR-@FQ5}o)$B5e_lWhQRzErJm$>H~RUdjeK}nehDA3Ts_(11#=D4-|9}-s7mT zI&Op3Fc%_+lNL$u50mRE%0*1`^iWe1XCnuiX`BQImNwageC zt}ulvjNaLAe$uXAzTG=}t#ax(Wbhe11ws}wd)iR88?&SP+$3krR;}sV(ycWsimF!8 z*{B3=0x;_hD(iiEkq(;&eL-iyojH7#HZ?XFlAzWd(imv?~OnH`0Vfh zICmk7hUD`?XToFHtFcz+xhERG@iV*s`R`R%c51Cfc923WOCkd~Hu-|v%WH#PrB&Oh zR+{}0nM~ZtmeM4uY#tnJDG5rl?#CTaxGIHSYQ^G3Zest;QG2@Xn5e$wK%4S2(+hc!Q`wu990E_zwneK3Ci;#dC{-5fsu@0 zhJr-&KFecnGBWW|v=UO)T2QqkW8;N_RG_e8fgqN_qi|*$eWk<^@J}3*Zm10>XJw9b z)Z|eVtpD7%gh%uSeFg-MiG98cO&9A7XPdY0+_-#+wU_I+m!~`dz-b4!8I}g>LE7}8 zkC^KJ1_UQQY*9IpNJ--;K02B@7zwcqRd8lcILnaSNN2yYe(UxdFTRZ6#~yu@RsVDb z2izCjW3GR9XM3CHMfL|hX2~0E*7+>7=NFrE9d177zJ9?L5O^C1_UNZGmJ-ocZm-6k&~xpvPNIg?Ze?D+~&wF6k{wMZk1z-4H^`p`wL`d?EoqBPwMitfuN<8-|$5 zgK14~_PJF?wZt{#byJQwST%_U1BePlkVG`+f;dMr=#5SYqBj{GRob4S-ymAnpfiqy z*fxG8Z$uvEM#$)iDwc^jlHtNBiKd#(SpW!A zJoGsj14#=wg(UhKRRV(dphWQZB&TC}42Aq?C&ZCC7!)H!M;cx_^n5gsqPf1Xmk@ef zY8Z9Na9RB%F??Q>_?^TjZA)aPr*IDr;nD~vHr5SZm@=8eiChW*y-lpjAQZ)t z7~wCh0p4_DIy5d+BHgES9heB9v@ZxDF`;K4y=~eH{RP&mdzw*W7ygLWh)W$PSRDDNt)svaxeH8i zE*SU-T)uGSyvRGiq1Op=EXK7Zrul6;co5KJFx^?*xV*7>uon|+hGEJcR=FYTv^{*S>4{^-{xqS-W^kGHMRq4x4VsQr!&`WG`&%hQNrr& zmGwJ!V8;qeWWwg~NBHEILV00`lhHN9R49X-3D_cTQBbTnVdk)U)JJ)>)z=6H+hu+r zB#Suct8d<2VW-iXm*0Evkw;EE`9!Dyy{Gb4myPXGsHXk$m*J1dC& z%EBbZbL3v+XZFeDgcy13359T`x}23-6ovA6w+L*)|(a24()W)B$ylI7U!m81T< zBYNg~ZIhmsp=w*eC*P2PDd=gnKco)bRP^QwIznNz_yH`<45A^VVq_8!!fJKwp01-0 zJzPD0fH4gk;;DMMC9AEsZcbml$Tr`z4$qNd=>K;s`$6_ zmP^VR&M*ehkz|ovpeg9Y;9%IrT)-bCHKHktbBJWX<0r8h(XIyoC|1RorUP1jp0g&N z1;Re)$0;|+rcWG)0c>QZe@nG|)jvr**tR;|+^Jo?tLX}m(KcXJx;0F;vk@)r;?plDkdz`qYorN_sv_0r6?# znWf_q9jhyo|Mf5GPaGM3^huWAc;jeB^$>7*H{RtgrM2-deX4f;R{wwdQhCsum0Haj z#*vlY$_KL(jNaG@_2)k`$Ibu0_oe=L)~Rp@huM;xHv3jT-lA7v5=0 zML=VNPd+sM(NjIHPPjAAsM_t64=GM=7SX0y}_0N{YQ;^EqXuipwniS#;HH-x= z@S4-?rq~OH!}}o{0npnp21wU@>I~!`J5Zv8FIK06{(JAdefiBd?=CNo2YvV1Y%~!J z3l%+1!NgGF0J?BiHiXiV?ZqgJQu-H#ENZye(KErRD;KTAGgVX&#l6JJ>ZIDn%F3Bn zURk|!yV+?D2b0~+&EDqbpvOHdJ?^h(z8@0!Xsgqn+jnr^!GrVr=G)!5MhnYvRfExZ zi6;zftZ(#tJHwrw)w?U(ckk?O?I1tXUYY9`G^JA!&-`ch+O5g7Hm=_7LLY)>ny8_YLz&kFwGQ2O3`;AX!Eb z@kES9A{?b!V_IWtuJnKdu|-gc;qT zNza|Kj*HaU!8 zO;psAqWn??D6`fQdWsDfl~HZ%3mdVqGa(7y+;p#C$~g!c|3F1u5C=^c;7J2zIK@OO z8rb0!L6}=44W~LpXj@;qzTjXTj?I`MW7!&#LdQlB+Gv-P6v5B*43tdyqz2eXT~!2; zsOOWx6mPb(q(S?ddY*clHI=bICKlrtT&xx!gcnp4vG7Qymh6Zh;X(r1VHTiI#@t&utuYi~$tn+Xp;JJyI1PeA88qPS1QNX_eg#{2 zL))Ievc#>aqB|*j#InV6)t+7@6?Mvd06HNr6ew*K1jRgBJ+7e!rhw=SM0$*Pg&5K#|*n-`%|>$J{Yp8mJLJ8k}Q^VtuK z8sxoQROjv?IPzu(m;PS4L?_O>-Kv}1rHX!bt3`ugrx#VK4te4%om0SS5h9HYj<~fH z?afzFf}~N|Wfj?!2RpGF4*`V46Y`7wB!odkFJ*aMU)YQV@=JR!9Ui~PH3Z&{*n4L7@vlQBI6W}TqLRYQP5_DobNg$%T(CEb-Kod z8eI#?3@T(Zdl*c7FAie{T~D!1>S96WP!@Q;7#z=fOr&;2>!%{5Ary~7~K?KoQy6pdMFVV(_La9>E6 zklYsnrozUxlE$=$505If4hCYd$M~}|SKYrJR?kVne4eBP@UrE|C;+e8CV!<4QT7 zydcT|06+jqL_t)ue?F+@h6|#NLrM_ZG;6eUMPfpDMT(Fpr6rL8FXFYm&8)fs!Pfu!^Hn6uprc6oM3| zXbN!tyQ^=naQPCMFuP^Vo<1*^wfrhbxNWa&FuQ;2(nT(P*12Q6*{0=U_iF6Xu;{o#Q>u`ipZE<%wNsY{+eAVcNc&TU8qfTPUX;=cfr3E5 zRSMaW%P)AdnF-*GgfC%F7I^pZjjUjpOl= z&JZ60X*?qae|}VpBCDrZ|EY>&CGu_IqDv*rKfdtkvUf zv@>wy1T$7N&h%__eozW$?VWpYv<2&;0YBD6>DUmggvAJp{Zk_SALKIOp{jxv&Vp!y zW=2O7Dv*?Xh^LaHE*y!gP{ORxS1iG>sI#re4h}_ls;jIM$SQ;*P(*7ZD!z3_mO9aA+enlvgF}8=KG{8dLCw(Ef)2fxtKu*Ad*93f~jPoiPytZI0PKu zlaFLk1@w{KJ;~{3_+Esv4&^wfu!NAq9W;DL7CqLgkmdHyN^`v1zjpD`(UYg<4=iQS zjmDNA_J(v6=s0?Tngy&#<7G3giOryjTjM_{s9V_Lf)OzRU6PuI|HGdd{p}BxI}5exU^wZS5baU^xb8zBYFprheWTPXwLbry$`@aq&Dym2 z1g%zg@6JpY7(ApLDlsoUF7@b=VidC?4#+_u)*VRB)%tY40}>iMRyFA$G1hWt8wJwc zIc9>Wg{GB#{5gtAeeHR>zY2#^KKqA8 zzNQ46f;nQc$BiJFRpJi#tcij$2#5R4fXCRPY6L=f;?^6`6gr(t6OiLS0*38V*fPkq zbK-#N#H$Jl$gI?9RyqK#{gKG#j87fXZl6o`xrcJVQnc5q4}YorpZsiV|J?dyix6iw z7=j2Z>Ed30*_<6eRQU&gXL#;v^R27H5%&YL*q0m7%N4eFA8kw?I6VE$Uno6ta{855 zXFI#q+9KHi&ovihgZgQ65PyCn;#deN=qAe~$$uDpaB*0$2hqC4da4g*KS zSF;Pf63WJ*yw07J;gB?~8r7sgQ=>;oMppqG%EBJgL|%4{%PWomq9 zv%9dwYyxFr-y8I9UbwikvDxGT4A+^7d)TH6N)p4LP#3S$2hqYi(BnW_2)q+V@fD9MI>?Kl^57ndq`g+TgK4AyP8uqauL-c?GU-UhOV6d{F(5fQm;B86} zD}gbp3!$Nb9k~laFSicbW|IJ#I=lc^4^o!a<7Enkq9Y9rk>Hd9=5BC()dZLRFFK=P znlM5{o(KSNNFs22rzqsY#gv3Us0%jNH!OjEDJvwAp96RoMm6YEmoj`>Tf?B7MiCZU zQ|lBb3(+|^<0;=OachIN&J|j36m01GF!26>%F* z0r3>JMgqjiO%f7SA;$=Y5_-nmdkw8~K03CQ1(41k7bVGsg{kEp#o%hph7|EHzJ;-& z=Xwt@Rzk{aa1p(Ld-%OS{@7g+?h%-UQnQ(YEhKa z9`TdPy1VCTXJbwsjuVkXg~JSB5Ot4rXoaE;ja@qwid+JMj~To?HisEoL8fymef|`I za|%xdYZ1z#hrApoWW8n_g+U2zL5?yQi(?exn@1HsP*Vjj<+t#okFvOE3MpWv=ml4p zqM}DQOyELKeF+bnfCQ2h@bJhFnWF8t1sauVJ8SD(Yj?W)_Nxqi77XFT%^=3YaYiNz zW2Ouacne1i7{2jUWElB+O-$tNkE;z5LQBn&I4Y=R5JGAX3z7B-Xy^e|4p>>Gq8RS! zEhq-F>U$1UV}5^_mCz1G@F1!1U+_%0o<(X?DW8x8(UA9GL7eLHBe4-+l7ky9BN9;3 zy}Bpe#tUbM-067pR{66(TH3#_$)jPJcz3-Gx(ES^n3Cz_qXrSsgzH2b%ooZWX1CbcZJ_+Y}URvmu)(aL9@8UNuodgJv)Hg#dUNA3`1sDQ7C zSrHe}H!&K-_%^OQc(i`}z&5msm=bXGtJN5)T))G%IBmAF&r^O?lnc40KIBHhpv5LG zy;AMm_3`?4ec{Ni2BHX0vPP|HztU}%kU6}OwV1>_Bj$6Hl~ z$=mH+6GvfrJeBkX?u-z*n8@x(b8Ik=3Z{O1Tz5uVJSNkb+7(BFpn+wKaE1~ZPVHev z=cMRt={r;`qKX3M2<}VpaYp79Y8mfTH2v7)GtX0pW7=I$@pd1Y63Lm8azzX}Iw+r%!+~(AX@AJT4>!@ftxY@{&=!QQaZf9Tdx9^r%*xvXa}fn0rt;{U zi|GhfZ0dxIOvUW0PO`vFd;Qe@_S7<zQDOHr)AD_I)MV*lbg zxR&5IPF&ClEy=<_r$kshOtxeJ$T#!?2H+EURg_ewaxGxfg&K?tIOX|7i&0~3 zU@4k^0FhO2qXfbvPSn$!uFpFdw}7RzNP-n4?MI0#YD7)95Ri?^0*e~a7so@r8%@R2 zut{0HNEm!@JR9hm3ee944{qLW6Wtei%t ze^B&*|EUOXKa4~;kVRr;QQRV3{!+QG}($Ysym?wi_c1l?7RdJbklG|M8h zu1nOYE&^zNO4y1+DOmbj)QJ^jRaG_jFF^YaOS>JEEG$ojq+yB4HdWz`F$!PO5OJ!6 zpe2f6lYs$M07va0tK5PODeEC>LJRdmSa~C$>PG=YV`k2@s;rz>L`v}!0rQ;?Gyom~ z+>x^OhJj0lktX?1%OT%h;1yH5u=o0UER;QJ%(0NwqmbOu9IXSwqAIDK-iSvi!wU(D z6Y9e+EyU?E7XHC9s0DkC@s|IpCrYOA8Lwp8@uLR(``tn_XW)gfK>H};V( z1J-|xxe3ta#`ssY)9tbWoX;`v4i6PdlE)Vyp4<_%@NI6v#hfHGcRaC?NuF33d9N^N~w(evX$34lssC*99B=JLtf7mwIIP+YM=;56ooUW!YA*A%Tq|xXm_=x zB-q;qo3-SrU9}2F!6JTQ9g)!_OyFY(7WYpX16;mV{;xhic;(voXP<3-^uhY!dl>JM zgGXHDbIpPsz%q|Rsk-%OMRB#%I+KrX+ z`D?WY4sns2b^Z;~X_>o4o6U`l>c9AU<;>+0k7nhTX|0T6tZ0fcc!!$_gv~4I*-Ywp z*%q965a<56R)!Z~122m5ojVDbK4#M*?-!vO$)HHl;E!qWbG$`yqNffUpD$F$h1EYP zn?5?*SX9~Sbi`IA(=r%hhThtcXY&oAZJvuo;@fzX6Y%jAy+jAVY4liI9u~ulraX)n zAK<>2;eG(C4JQNkMCFQTk!xKA<2`LWP`lLRsoy@lmyS8|(?;qn@mrnGFvPd`XOwam z_N2eO$$b$uo)1A7vjIc2A^Rc^Yon)2ZM!0f;PW4RUYDh4CTsz@;J}R&)pC2TwY=8< zAHGmtn6H2K6WkLz9rY01(Bd7}9f9v)+Nn-|`IEz!E_J^6jp5E#r!?g5fVIE#LzVyX z7wgY{Xq)9-fAmuG<#$J=`7YUGZ?jNX1sK(1;iB-8ln7xw03Tql5Rw(mXr7goi<{=~_>W~q-ob#yu#>=j){!Em>9~REn zS&Ej)J|7N(J*aDs*LaPj33#DJJ_Ak=79phl()J4GbVPg10he_y`4Q5cL|E;fla`Qei}y zDx5(1xR+;iDi|6OAz=>y9Sm$vn_9wv^N{8p!U0DAkO%+CkG{K_Q85Jov3OCU5KvG; zMmOY(@hmmUCjVN~OeI8T6BAssYeSE2_S4Zple1(o*)S zL{C`6g)gx+Ql$}4e*OpsqnM(CE{;eQKe54;sNruFn*qxk2#ALw{PefX`L~zHIUdVs z9T3$EEWmfuygd!6d4XH4B4U{08wF>hKvP*l6_QkRTTL%3^@(7z=R60MJd840)X$K{ zwV=qUHi#K1je2W|MEY`$Daaf0K^RjNUMLWhN8fbA{F>^rMrK1A0=Q0KbzP=eOwqy$ z;gm=|aU0%IBz>%m(r)#!4YCA!3-VbkB?~^gd_9}H60K()>XBl2g?~P67hhO zPaKJED0o7~rWmd$35l3i0q}e&Q^YA9{H4$yR;nR}j#vxIU_uOtq$dKl=H(B%2+t!MB5Sf z*Pv!Zq>{=hhoMWr8Ftk(Um$7;uLNH8NX_Q zd{UFbN|0=3CA88VEiakS;vlYqKJgfqyjQp;U#)CxH$V5)%9md3KYjoBGe6Y+*ptXFMN?o||j9Xr;#7hskoP+3I%f-R0?-OO@}vGy1{J`t>`NZF;REo=!Xd&fBG* ze45b|>R>XSk~!rWs5}O^KJ0(u*zEaV8SgN-YgaFCcK^xmO#ke!n2F~3#AF6rakYz! zATtwVdG40Nk9Z1hcVYIowyfMvm-76j2pW| z83_|YNK#(goL#=nlD9^l;@}RqqHdnRtB?UN;t=s~EYN15+u9D30W6OSANa=EQhd^^ zH4iM>z2?49_G5K2PYnz`C5*Qc5MVZ)uU}p({nJ0H-7{bMi4XQ_?AFfh84PR;1A(Dc zoc2ox_f`M?$A>S!KDc>nI=58+?2nZH)1R+Ad~_B2UA{W{*DqFgw@b|>Ew6i%G;I}W zsf~%u_F&f0-QDum)~vJ800E~D>*YbU#uF;O^h)gyU#y?M+TR{5*18p+Ad;NPY7kZ5 z>oh12KcTKu*lRX4JqU{EWV*1gVS>~xq*qMbZjNB!kdoyxGRC8%U?PvM=UBu*qXbr>IznI^ ziQ9zoQN?*LFhufL%WQrb#~Nna7>W+E+EbOqDF zjSxVEj+9oXqB#>E>gy+7m7_5M8u@AsIEn!6Uu>WyGqtIL3K|CzWqpM={tP~s{E(^) zrVuCJLMu5-BTE-MnOn3krBq40-lyVmmBYkN{Kf%+%lyZ$Jv7A!JqvL4Ew6ri5Z&bv zVHC+pTF{GYiJC*xqH75cnd_SzjtA=b8W{)B-X5e>+B7xfB?_h&NX6F#Nkx%O%@7v< zG_p9M>5xZXjN(=cksPOlR&^ay(Ko@0_D}YXqp1F)rGlff>)dLJiA_N0OBu1Jn%IM< z0_&~Y~rD)$OUTXC0UQa*d7q{Ae^9-f3l$W(^$xM zl273|$0}#Vb_Jmag9;%HV3K8paSY4o;D{F$EUXt{_^BSUlY|%vkOzy<5DhX_JOZ2&%o;#gsArFW zh6GguDo0eLI3!O?Y1uskU^T^4I*Q5Gw<6|g4d9CIxh6n9@?5N@r?{d@p>TLBK{pP> zk*Hd@bb>@|$k{fmx(&jJ0F-cllcT&5!|6vkC=WBE*iDM^!XJRnH_fvYX zb@r&Rx}L9&NM{N`KJ4yVLs1v%^;|O&@r1c5HEc&*E&ZJ?%C|-R6vq zg9fA8%2w^xMwJ1;t-GaL>y`ChaAR7z%ZiWMV7K#?*L%PEsqRz9cA2xc$Ac)NJ~wRA zUo0;IVy!osySZHc^64qda!TGq0zh8i=0lSSHVMg$MSZS1oIK>{i?#U~9nxvVhh4}! zS1td&A8FmXJN?#~wEIVWOC4O(_)dJ2Ej(xawaT^-6Cnmo^5K{av0GrF)dy ztxK1yzx_>~B3y4DG6hl;2(@yc)Sd7EC^42!p2w}(5-EWI`rse0LqRYFPwpMA!<@U! zx$^)sq(|q-6$6lcG9M%ETG`ej#y3G?L+YoEfD&79VmpeJdDyb2q-{zsJer7!%VlOJ zL~ter(BuY_Ao9miXLFg{Bm_r9-=uov(DcDWY-Ng{WSMoG1z8Zv{!6v=?(O;;*9w2P zo!1%naxGxMLs#+$o^Mx_PH}ms-xvS zT+rxqb!1qb|ATK2zw*85lt-{)EuQ2iF9MP^dr^ZQD#9K>lXtH+UpQa+xeuc-6V(ky zmS1>r{C|9@@x9AD2fD##qH_x%&*TohQ<-|ZGcEwEzL_cG0G=u`wsr$57!hC^gnGuG zt*G&9Q2K>tP^Ari%EdqsG{IZOriH4Iz%?vp>Am~vtS^A!PMNqJEGb1@!&+_*Iv_jB~AW%!w>7gpHm6l$h2`m^%~$O3P2HZo9*f(joDDWf~Zi}W`=i9+e$ z%9qp0Gyxcw1TZ5l>VYi>l)#n}f=Q43ww~rxTRoC*`Y*(20I8)nqzjxbXkcgZn#KgF zp@9@mcry%shD=Pn zb8SWzimDb65-hAk+e|15hC5eiCcPYb(FcM^8nT7v^27%6hEj-F2CL*^r0jtJ(1^J7 zWc;qD6ogC@RMM#_$SC^pcHRJOR91JmQqg|=7NL1&H)7nr9F-LsoX)WsAF}Mrn-6vq~53r3|P-_WdI37 z9NamGL=e@ex-CF^orN-9z0@a>KH$?K6?wu;J82|^gg>QXEFFuY$ZUh9pr)i{+LLzpUQ(z!rLQTlkV)-7u3Z5mW6GKubd>T&r< zZoz(fV39;2gyV96LK+kKv{}(#gowFNhcCD#j@404LgYxEms;}X^<-u7L&8v|ET+M(wJ9P-r;P8G=cmyttp>C$iO#0B&M-r9?{hb@T79$NRuReiUI^A zuU0&FW^--j;N1Pl1#zHjEiBw~?B2DTx9kAHSChvQNI{YK4+dpbo&kU)^btibJcF;a z`K5sTi9Nm(R8$Blj40kUle$?rS9`P_E|fTi3 zYs-LnCv}zu(JxmkHd1d^8}rq2lZQ58&~bSGG;dT@n_22=Cu2T%4^rk{TUA`o^osHbl9!5O)oqaYQl(@3wc$R&$40Y^vX(g ze}{*FPOJ0v!L;#zesl2RyYbCMJ?=(fu27&xHoD!wc!GIUI^hM1Pn$~23Gw;EVFh_(60C*(-K{NR;tfV%Ga z75x=_OP{BlK8NwFtsx8F-wO)Gw2%$EE3ZE03rN{i*iT>Fm}Z;24fp+C$_P$E!oyKl19r&qisgZj7GD3U+>+yuKV<;znSj$ zeU@|XbD#5^=XsZYd(du-c+h^2u+c!7l4(eV7dOPn?f2crXruS7r>4L0xz)e>`RTDk z$l%8Zc`7IMBm%w_!18jof7t#SM9=U2hY|cg(o5KU!kG;_PANTbxUY>6p>N)-# z##~t8rXB}`e95Au_6CEO-e~^r*GE5i`{vvC&nC0}r~mKpW1l>K>O#A{(d%#Vq%$v6 zxYVE@eK6Q$&R@lX#Ah)=d4e$m8tHmg`y7Q>WjpurkeAH;;($vCCB zV@Og;7y}P&G}|}X&qaZ?emx@~2y^+40KG*|^S|=hW%ZpliAR+Q9p*f1!B8R4OsY=x zs1^%4oD^c2+_DRDtHA4$%)I0-TszI8heWE$l&IBOm&aro!i!X+P~}9vyD~^RuQL%~ zF#(tlG+uqR-jjzi&w#KrLB4iO0H{r_HT=rNfe>Q600S{>>dtVA7&_3DfyP(wY-=40 zZ^DmFm1sHARmUH)Q^C8|J3qNfauM!9J#Uc~z)KJE}9|KoRwG2KP2iABGm9yw~ISo`gur|?m5f|^YngPF3X zHKsR4mOiCzL^{6G7b28fQdNfd0CtV%u}WE#bv4X#8cXxUv{plh8a(3hIEX%ZSCsNV zN(cF22P_yulxelt6IGhbrgJ1Q2t*kJ{hCD^>1^nbzZenw#7(6)P#6UA!$i%d18uF) zJ1|jdCnO>`!JLwOVLt+V6M_RoqXo^rfn`T<#7S%+fJw805V_)lKl|B#{on4vBPSV8 zo;xmAN2aiu>s%foOUMppiIhBjj8^|gBy{Q{)bg9ap756S1;l0`D;h8>-Ha{BGl!{G98D=2b6_HLh)VMX@2f8|Mp^V_9 zBMl4+Ryv&4n{_rm@&7Ki`psYZ@xhV3mpXiJwbwg+VfFj>b-wh-;P^AG^Se_X#>q8- zwh!CnA^sjdgh)bDIa=(CExxYN*c{M?g$d~SONKN-!~J|Pzx1IydPnzdJo#!Lspl>? z|Llpu7r(o>$Rf7K1wRkyVVo1mqfYbWYn?xTYI*z7MSG*O8g{>W-_EDLwY6LgHu&*q zb3YWg?lQtmq5N^KZq&xkXG+4ii zTbCuOjA90x%a|N$gjov-9^^6;D=EAPEnEbvgtmV^Jli4EWlKS0X)C1Or;=7rIHZuHhZ2yh%VJ)=&|Z=2~tPQ(4?lX%R=N5GDGI zd;~x$a}I;qV-#IxrtJ7^INBhItfJt|)3l}umXuaJkQVS4$I}jA>+=zyFdPkm3KVYpnNjG%Aoq~T$X`$CnLl-@iAnFUM2q1(Z3`BidZ;5pkjEJHK6GR+ zW|P78*3~!OxV`rP8H1)iV4KG+@%xe&-?Rr~7Hr|u(FPm_2#Y@ru5fp}x@Y4GHwOT< zScTCJib7xG2?Uu*C^L#VOJiu7L@}2^BZh3ak0yhn3e1%DDxgwAq zQ7DI3EpE&S&}m>HJ>$;9gN#NoS7S`6j7vMqzytCajM?IGK4JwXAxXGX3D{cLy#ANkV4AQ z*P$Fu^hGH9#wJ_AWJmr}YBRfomGH${^msTW&YYnZy#rT!`C}H7Vxrw}qbzY!V!;%v z82dhLMC{~&A^Eh1h0DQ-i5HPvlUOwE(=sp2$OUVn2E{G7OeS>8Bwgh?P<0lyyob!n{PXD{^DZFO+he&V!~V3(N%v!k%1`^$`}n4Icc%}(aOXS zmV!z!MvT3)g>EEBs8tzdMp0N<12^1jtkKB|cr|vQ15?q9>34CgrG&HyxgWxkTd!ulsv{<4p2_>cbc0XC(wX;pkk#1ix}e@q>J4qwklRl7#VD1O&~`IzOBH1Lu{XD^6$*h(A|6@yr~$Q6by-4ZOnmB3@c+f$DG zhCKF$!zr41=34KN3CSo-AFj@%JYLd;L_Z&48`HU3;a6{Is%H!&$Y8h04c7Wz)drXA zTU2Ho5TBMCKe7aKw16Q9TlJc~%jYKtHX8rzCs!YO_oOlAN6MTS$qBU4UZSDTFT-to z`O*0U$GPo@Up_*emJ34u6dHgjPZ^M1R&r1MqCI06RDj#baY~qozqLBNZ*_3b?Dbs` z**dcb_=D8&p>U0vLB*F7hMS$yZugVl-m~nCfB9!OZ#sIhadB#3uO@Kfn@ zmo*8|K^sEB^#(&*uX%JYm6Y3(O2#tr;hA|G+6T(?H#V|KA$$2t4SsU*qH_}{IvHoI z$2;B2@98TwFqZLV3@Q!Rn6;yb43IUGBM~GH&)91Ggb4-cL#yZfKt|{Ab=Mue{yH{h z{I)t1y5B3GoIi7h=K^V(X-W4)JYBup1waWG%Q4U|r>H9Lu3fTzaWB&YZ9Qy2gri2l zR%p4b^I@vMe2R@X1j@Jp$2Lg=UYuy*&v4?OUAasM-|}4h>1s8#NH>ATZXvjZD)XR6 zGd7~gv{l{N5n&Q5{bg6;DsyY8LS3i%NqMl_5XWJyFxe5I4HU`-;gM73ZgQ$cIIfZ+ zq$tXmG;pNW)-E>4>X@t^nK4q4?_*baSxoY!M&1#YM(AbgGeWh@ZuE%l0Mi$iBTux% zKEy}@SPTs0nW-EU8bTaKjbo86q$34hVIxtaDTZ;Z)uHG%p=B0uMA1Rq@f%&G6ST%K z6y(&X+>1#mJ}(GhYRmXQ8{;>>9)uJj^I=3pgk*>}F~fH3&18$f$dHn<$>GNxk5Yvq z%?pmeb6A5ABSdH)8(X z=z(PfG3`5!jq-5gXmf6Y5(RVQ3d{^4qnu@gqD!yaq$Y=sKbT>)+?hU^=Go;XiIyr( zTN8F^6lyn^5Z1r+H4<^Wny z7;0n+3tb*#gn5`MdJA85rUC<$RXh5fzSbm^3S^>9yg8v!!`~lD32Xu>`6`QHClxs4 zzs(H|`f^jLelhNiRmXD(RZy}{f>=sm)U&h7r5`1lKtOle=U;i{<>ycBJAAm?|Yc0sktTs@9DYz2ZSa*`2RI4?^hXz;|9o;AJr zX}H1?)$l;aEdNZ3fDddKVlz=n-vQL4vkE^Ws#%filOUWWGAxmp%YZ)YXt@{+ zTcgF`Q{UtVeMi6alf65x;dhv(%`x95=hP90XIRGsv+4{+Jn-j>U*GuE&n_;{``d>+ z1&B_w4lh+`gLJx0_^>kId&P4eT07}1Cpf5VvUVjJ1G1jTFe+0ZzGVKVz|yVpC7p9>g#U1ZDY?~1^^ZHwV!jRUwi%K zm-%%tegPoiY5x$WPuSo-xp}MOkvCR^6om@Y#Venlh;Wxef&1epM7FzY_9YI?F_pf` z)o>K?=P8>~6j@eyPM9IUr~VcxDvVz2RIM?tnK2kzl&B8%(ZI)_Ll3iYu60u>I-$9N zNi<5JM6@Q*8?Ed{AGOHlA6E%er5Bvi+~MCk>%>CJg=h>Ux~RnqAg{58)n#z%6`W`Q z5nsE{ zDAlK#=@ld(eGu|eOiRZc*3m5sLMiqD5h2oV2j6MsIMY=cu1aD1X=gQtSfqf{ZLM$s z<8I|u1EnUM(paqsQUv;p7j_|+ zd4_SRAXuf3wndznit`#QYaBUns2{jgoU}!#q#2gi-BZf0l^YQ4Vlh3{V*!6F}_F8_fl}FUiMw88b z`)+vm?HA9TyYT8O{5~nP26fFez^mn|>o?P?&@s*C0rGlu638-*z8;~YLJO&_wFZ`+7SXQF}#qBaqrG`f6i3ZPH zrvXh8Bx<(C5V4u8hct9@0*|7kktCd=IEWbbp58d5y||}6|ZU|jV~{*Sf(`HxTb zblZ#Z1}kl#AVD!sI+ONr@XYD%Z+@Zu{ioZVL6>V_3{BMI4Sgg4qXsx$(W9&3PkE*o z8apf)?Nb05J0edf=9Oh@FzD=K;fE*Zi(b1;aHcE5h#%S+(}tfSj3kH7ay8s)jNaV( z-EW+G^TOuOe7JeK6G;O@UxdVAv)aZ@<33I)d-R-SU9|MJ%$hkfJgW0C%}0f zhwx98ldVkBQtufmVMW#9!Z;ydkwRUnF_@rI5T!V*76*=GN5A-7%a9vQ@JW{m7ZVr* zV?8@U|C;8YvzvT#{6k`(7wvo@PdK4qjSid)J6A~fm9~aVoz&0C-zD{zv(vV@7cQU_S@ce%{z!XAN)lc<@En#{K~V>ym9&!elwC^lu^rC zspj%0rMz(z@5vo@Jb*)}#EYLqM|=@f?)z}RbzaxbSRV_V@1T$}&}sxpzZRbiAqq9u zh;SHcAdXYLQt6d2*J4#t;$YP0G;s#5^M9I53#6e|8tPhQiNTnhY`2M;5WznaiLDW7 z@-_u=bBVV`Ii0`+!xw$T9p?my0ds@}BCr|=)G3%!SBKznQY;vd<1vgKMN|HfumHnxBH9Q74-!MJNq@bRqS670HUxV5Jd0`BQCYO8&Nx8|?bzbo?qFAK8COHf zIwZo0u4xifnzVd1_!4u!3U;&@-jJ>EU zKhRS>@hDF+GUqeKFc!v3kFgx$y;xvBkzlRKQ%n3yqKb)rpD<>y3_~u$=}dDC)_NVr z!2+5P8w-dP!V|kVG{=46MHVS_gu9wEmTSaHbXCF$<%X60bpfn6Z}@cqD&<82HQOyo z&}IS*Z+ox}K)6s$LMMx4Jw<@Q01u_;TsnL1*+(88Zf$Sw-^ZCJUd>ta$g%4#zWeOs zqtW=%W$$rEA)U;S>d8CIytUWWsfCF&8dODRyyya0&*!;eqCk2`yri zQSY2iO^#`UU6u(GRdGe!nw8H?duM5 z4U=X(w4ZF+?W1gqWn2I9xcB+{Cja}}vlFLR=cnA*z1rR7`{b=Xtw zc83`O4X8mIj)yl8v}O|n3_sy~8u2X{9=)1C==PS=p}*!e;$8<%?@|MKvQ_ctcZ z_GaHmjRVEVI5Ha~{}P!dKELikY_RDqSPdSayWlv7g*_wC0~SJ`*mGiMX!9!*Osn%z zV>;x(X};QQH%CU46R;s2==($Ey51iMCeP`Dg1cRHyslTZ#VW>XY6WPQdF2d)sL3mpz= z?Q>x3+@_=1p#+!oMI~Su$Q(91VF1)B!y(97K2eB>XceIMiK7+l#zn1;D7F#Z{BIN> zJZ!SfP(-v{Q|eRLBpiQ}7Kj=6v9|#jwdMsk*Ilq)2P*-YlpCM!ick{VVAB%?Eq+9W zW3nSRUVrT7n>p)0os5VwR{S(L{oM1XpFTO6jfefMiw3hrB%l=pZYNjKlLFV=9z|&Z9WlxeNI+gt(cV8p zQxz7nZ_bw>M0ticAgm56^6Yow;ok!ogBQY}4i=+E)|uwqHi{j-9>4i#`Tz{=Bc4qM zSoJg7Jm={uKo;quY#JaTAd4iO=ZvR?B=rh^F#w1 zDgX`X3+%KA5e`&&z{(h+caw)>KTLzmaR4raGiIRy^&a0S6+#f~ctj3Fz-Cy{NX((B z7Au~+W2Hwt>}tIz!2~}!x^X9vU8GQ|YLhSiH2i&WT8<%}r05#!5h;{xMm5fHbW%%u!!$SYEub~U0j;EBAas0MWfR%>Cg$BZIUDACa44n0OLxc3?&0m2WYyiNgqh1C(^)Z zgdviSZWN0;D;?>iW!Rmyhhauz<(R~%kyMQ#!nn!aRff)#;c&vHiV=|3jCCs5#?>fq>>E`zkeQyE*0Ezf5zS5vyPMpd@|l@cs$ z$d1^H5^ZPTvoPnDBTfiTJKIfeRN-bHSeVV0ub*9h>*>Xp9&G;M!|k)L4VOG6hIy{r zZf~`He0@U1TmLmFqbv6`Yf^L3BZ)40ySw~W5jPV+J3u_EE-1}}zH*IdOuqKwWi&eX z(1@pWG9x0HwIf~7$BL3Wj#%h&wPR;-!_})Bj*XTbeo(7<;?>@8t2gJ)DcaIFGo1{B z0%Ey|LgDa}Uw6ibJo?i0ng2XOq=Rv0Z!}GrifO})neQy18WGg+vt>T!sM(luud4KpDyvFCWv5q<{PMd`DMV3j{&4Z}FU)pY z!%e>S?JssY;cBVL7W;8HNIWJsYpv5A$4z`OWH#lC^^@ricYN?g|+FOfAjIhzxnO?{ZFjAd*+=1PoFf1oowP#`6HeTMBF3q6=1r7Ch!J# z{YIY=gBvgv+8`!_JmZWa(I?R$*G!j))ra(_JlDq&$2L_CvK&=YKxpV@N^0?;e0-^+ z<3_eryGCWanWjL*3z+s-Y64iiMkxQyD3T;y8po%(#tXr?C2Cz?(AQg!sV)aNG{B4_ zAwrmtFFXesT4+UWrABn=GulO$AXG0oykn!(h)x5Ow#bZ(25uKTaNV(Y-F_FRROYj3 zT*$pzt#dD(e(J&RUwq?SuiMuX!R#`Fpmo$3`znWfmafcnh?Q&+34>)5oB9PdeI7So zuh%cUT&94HW@1#DJ;a&RZ;?weLl!H@X|P$=M8euIT7(n%!YNK?Og`py*D=mR3`@v2 zGul+vnZV?OHpo^mkaZ5YWk>h;{X1NuZLm)-gHL1Rl5%zokFbwnB6pyvH$g%d;sZgp zadl`vU12TCQHG;+rXXAm$!%7Ep=y0+mdQ)(G(&_OO0{GJ8VV|GwALamlPzp@l!sTr z#nU-BpbMsJmkCq+NM|s?RLlrad}e^)V?}8NI20BmaAs~@^BkCW?HB4)7wHT?a*2q*$h+GD9s&*p;d@rxy}kmud?Nm?^A7yaAW+ za|`-i!AKQ{gj!ATc|lReTN4{FDABz_L_zGDa`nxtGp}HiG`TJB7G)4Bubw6rCSA=UKFo(L|EmlLUN%@ zd&DkMw9~+04O?bjB`V0f8|(^Z)(9GasXhXc=b4iz*(q)j#@JR5ZE}*pP|TrqVNpy1 z#$ktzLeZ(T^{myY<0tlP@4xo8+xglhE34UL)$R^%x%)kQQTW;8k8?{ar?HG)c)+Zj zD)*Tc+5}?emz+$>oWw*3V(x4-Vl{+MNKBofrDZaxL2;){B>FwrD$BtK{k+L&VuW|y zA6k?u*BH#8F+GKA-ZYs?*&{1BWfGiv2HdcYTWP~fkJyWbHh0^>CBeq8c;j`N*;=EC zTiItLxOAH)U{QFt1;QNcAo5DREpsP(HAE&ZVZkZ)EIQ-L5I)LCY=ALAK^|9Vu$aEtoq%_M?b)s|HXG- z=>74kc4fhJyAnXE^#al6a9tXf=p@6A~K3f$SQkwxO+L zxY~EaRs@O|z@53pG0cR@cxNL^O<@^~*<~;jVaPG%#vy702Cc#Zy^vXB8i3m7UA$I1 zJ0R<=b1gAmRl>J3*;9_7z-yLFB9qx<`_RE#?|IMuqepmXt46fi-A;Sw;)SP^ZIi6(rZ3WST!sDUL?Mx4Q!!Xq2~kei zm(46c^69~{*sKb%Wq_o+$kKW&tFX~BTfv{)u%W{ECvFKT@gK@*tNq$aB(ssBDm;HT zBWLg|RHuz3uEI#F?Ib+QQca0P$zqYH)v<;><=9@9bZ(kDi{i(cCBj1-DxScmp7`?t zXvsTf6Zyf0L_7{cum@{kN4e`V)Vmx}n9*%hsgXesFc3h53pxh^ZlfwA!cz3{LY5H7 zTuQ_JTA@b07IEH1MzqI3ys89=Mx0#GRsfW01tXbaei|Ne<~=HnFnwc~!$~ltG4#+D zyBtT*?d31*1%k^t7XX@&H+6V~L^W^r;%5{!p39I*M>Q>ZiwxAF)tf^-0Em1t7sN@$ z+GvesL5Ia}kzf$$615tru}na8D&r&_W;8=#hIBPTinOs0Y?C5}^GcedD}PkeiC9pC z7r68%V#AU0?0414Gnfy!-S4mRF=<0U)3xuq2{XjoEY@M@Gj^}5PM zACSgN2q31?nsKVQ(hD$DBybWBsx)^95FPnhAb|RtWYUi|gQi^P1Ndl*R^-;$mtYKY z+|{KT>dg0~AYiDHI$Y|6=m4U6jW{~B04&Be{~gX;vF>q$!NsQ^eTd%yc>A$q{!Ee! zsm5^6o?G90538x?o;blH-&;ICGRr2TjC{o^jGKiCEbKFq^a1+J4y8E^U}(mWe8)uK z=oN>3Qe&8w^o%mmiyR{zix@e~>P97m5WrECNiJsP z0vp{qeeUduwW7A0{rIFO`xYrP@e1NqtJi+&rNzfSIoa&>k3Z9U?#yiGGGD$Ja6Y=b z(cI{9NtWMm?wXJ3?7cIx>NGDe&`RFY;b0Stxdmfg_M^^-41Gmnl&jrcEO(8SUGQ>R zoV#oCz^XnEit0=R&VQmek=y1@mtKdvT+W~GK74BVrTbf7cxdzFOZ?14tIIC~aE@-y z{X%4Pvx>1pfO|tdF-!^t6u3G0-)ii(7URZ*2dA3qIok9|>O#_sExxaQ>C$pBZeQlR ztbDN+kmeyW!JMV{+x9Gv9bUcn`sIgi?S15y#gXj^&pldQ#wi@hx~N_*krpzxID^!G z@TtbHf4=qEZ;y9YgWZ#>+>E6#Jo2RuTJXX|Dj`tCH2Y<2$Lhi8Agw|u3s`5!;gK5?qk+}r9hdc88N z<(S8y(kK3A1MYWiK{%tBheU#zwe_q!IDMh@z=`SikFS37D;J-AdC=X%)jM2n+B1rc zD+jtE8Yv<{gt-LSpyCsY@GBFO>KR4Qp}%R6Ry5B^2ae^<=ms>AW2rGjs|2eHmDFP> zg!+|G6;YYhgeR6fp^FNR4?PA4UQX~P7Cn!G)rKBi>RiSO0y0CX1;rxFF}dw5DIpj` zSCgN-)qkuvo|F)&!qf%|FhEt=j2x^~1ZIPlEpi6J{l^Qjbcn*pcL33z!J;Oc)6H{$ zoMo%&k6Zso-HnZ#@4ox$V>faMI71#nhH%A%XWgO{Dq5swN}8=H(y z*wPlo6>QH%u;4>4$pnpvE)jHgTjJ7z36qEk*OY-q%Ru2ausLu=kh;7>7)&}&Cbf_@ zNMeP`bj|8XnKaf1G%@E;t5=XIH&ojky@m!G6w!#2FfKrCp-gj;U};ejvSd#;ZAFrn zCMIYC8MQFdF2Wl~7-WF!Bu$W%j0-!W#Eig;S^|J~!USv6$8YB48=|Pxcn5`!1(iON zMc~ny+=Mo%;;($>r#omcLR>hQdhE3}bPbyoRhl9uLQGZx7Hdgu*)u5^_^~u*BrK6m ziKtL-O7&zAquaa4vZ|9;(NdVm_er;GU&l3Vu;N!!SnW)l1X44hCzyehu zSZ6AyvA&U9bk&jt+^!ihAxhy&TLBLhc*nJ*IXfX_9P<*SIuTBAgcyP~(iOtsq72K- zC#F*|c&bX8P{SDYGflw@mSj*05do#}!?yJf7X)-0?X$0(edeJDH@7zT9=d8YW>vSC zj#t|U58iU`JwSi?@e`{hw~69Ap=_rakm_VdeCPb>uw?JfD?x|4+ zAm5_S#z7%)<2%_vXzI{E{Lk3NOVpz`goD(xFt{7?DfEF_u7Mcc!j@ITPGZrI4ht2D z7dXlTYr-G(LXH8VgjU%RsU}*hZR!NF$5}k&hf~ad_$@BbkQrGXw%S|0uFnt^EPeTQ zhO<$zLo`yvKkF2W0l9ALxdjK1r;`XH8_103@AU|O#GTv<0EwE!ayI#cTF?tMghdC( z12DjiKnUY0pFE6mGCy&u^{0da_uSMOZq5W`^5bj|Qy28G((TMHkB5_;*^(Re`wi}cUvXR1 ze9&LL>+s~}Yg%{T(0dHc8i zr1ia%-Nj(Fxy9XXK!B7?O9_~Svfien6@o_0Vlr;=Mefb+=;rH|f9u})U%In#+p+c* zch*iJ!UGq*$z4oR+~$OecZoEYd$-1`jsBBQPM?0!o3#j7S{W`%766$1h~5oR>d+xxxa_2icts^~K= zP(VB0>8EPf8Hh265h?&iK)Jt;yord0URYzTVQ?5V(113Z+C2zFPxvrMxNl8&D;{(n zG8xVG;*)-;PV!^OIk?q|xa4FG0S!Yj`7G4%dh*&khA(q+u)unUf=exCGUh%_cl)_oh2{Zhr8 z?}7N3HJ+>R+=&y<9zQ-G&pO?KM5;*fSgZyt@f?{ov6RCtwy>RLr6AB_+xVae2aQ6q znpR%MImw7xLv-zl&?!|}+gRw1JZ7TclM;z{W|B=zkKJkc_dwnVJ4Na4? zndFKjIYbcUW?+&ezb$P})kr2~y4wkI$t6jvyrHquw!vCnaKN!uI~@~RS(TLJL39om z5Jq4rA1NdSp7KeM6HGFyZ^Nm6q&IumEv*PE4-{!D5GW$bRW9!dTInH+X5y2=S|~fYr+GvK zBA}RZ(_9G{s3gJz4ff^*Y$~D`V|=-9Ac<4D^-{|Cum@RQc9lY_{K-$AK}Uoes8%2Z zYF%(gde$g%09goD7022+^9ZS^s9XJGf#;xTxCVhtq#4tOf6fXBXuUuLK;Y<@3K_u18I^l^#P^b`=e*45oCW7Zq_;DwQt+#cBF$FE0EY8a0LJoiEJBRCzN4MkL}1u< zCe9#9HdQ*U^23EV-Q~FWZH-)zni>Quj}*lzVVG7@%2c8tnj+Z9wY4hT!+OutINc^{ z@LdfODNrU2oiIUZz+<(kCkwIIqg>R|36X+XuTR1uxS+&=fIYcl6sVFXK(I=&P|yyh z){?1;oBz)|_w2^@_8lMiU}v~7n~u1>VLWf`J#^%bzwkcdni9K&0o9?5di0UP@mNoUwq}@ zoNpMS%Hx@2MQ^(2Gn-=_{uos2d;%55}KbSS_JafABiElK&c>ny#GyL2!5c|!|)@GlnkehkDCg-}?L4+j}P4{bp~t+S{LR4;n}I_7ChC54$5DVoT28>(*Q!z$kxXH8~DYL(ckl zUwgB8-y^e6e{bVY9%`N6S#8zy5<&hBBe+*Jj}gi~HlrJC7`W z^q%=&y|?wj8#gzGyM9@l2j~;T;7vR+1HiKn9q_nu(dZ3#``cgn_Uc#uXZx8mv&~J8 zOk5r@{H27jH6iIAOh%)_U+;+AQ*R{N&Z@JqI=|CC`=-Z!!~GnL!-up~m=Y*pO>`N+ z*-?g*Hyp<{_=tfimz`u*J?j9mEt%191P5(E26SEyHR8<9#JeG!1th`ZgAp7RdJ|I& zTVyJ*h@{JvsI$;!H74|uxLn&wL7luX>I_9kwb97C2c9O*WNr`^NzqSgF9T@k(4sHr zs<55qA-~+bE1il)3J6dp+|AQ9EK3r9eR5loKM|{;@mW zb<^GVbT_v6dWR>BIz0~V&OCeS>4zR1UA)MVI-`aG#Wyq*$#dTntWvfxG^xS?A_a@3 zk?6sZK}T5B(2dbx03oX-V8FH*L=vD9256d?k8~pTyK2c4m~3D##o9^l>RycqhMd}< zF~R7{))gwXwShZ1Rawb4qu1iNvQj;=bU86#9)!6#OK9xtI%;yKA%-EJ_ng9Y6FZD= z6Sh~*lnVwuEh_#CFQ{p04v`4clhCsk@qnH}4e`(s!t|rgvL`l*$GE_7WZ1QGHpCk; z>aYfZimr}PLy__*1tYDD8U0alf^;m?2LRI_E(|o7&?D|@o}hz9n%8MhN0JEsqp47W zTNAa=GFJcMP$__28s;bkm}Ukq+}aFLMyY1;4;7(HX6a7XXtD?RI6uev1F;&}U~^M4 zLI_Q)unYeoiVZLrNi?yM;u!u zT8x4q61)nuosX1(!?m9>5dp~ze5yg_6ENy`06@NGf;PE2nSq>>hig`}mGdvkfH$;y zky2~vpfrqPu$cfHJXsLAVDNO4>}f~b8oM2*c*XvX*)@Tr7qn2Q4%tYo$+B#5ZO}sIM#IDh^#aB%y~$*YPzfLqH!0mzdM7Mn>NxokxX?Zq5zawhh&mV_r3_5p;}j8M_5DqtNVZUa#vLN)GW zh4rE<4@4v=Np3LWfv}uun?rDcE;Kcuq=L<)CQ zcrhje!C4IPo&Yj+=u28y2xh%dWI5vWy{STGN&(EVkE&GVT#pT{_UmmjD+@tgfQ61s z_4x7uDU63ON?=p$U%XAF(JIr5EM_vg*8J=}WXY;QGa_4jcM+-S}?YM^~sGx8iBhANWkFw(<9g0wN? zTina(&;97)-+r|7&I7IHlv`J4%yml+(Y6-c;e@G64wN0#GYF&#v)f(1XFUAWS9<^b zkESPHYW5HGGxu-?Io}jEVQ`G^;W30HfFvwrv5N{1i9PSxT=ETXz8pc~?0b7pWLhsHq04lVSuVL@?vNU>itBr^fm(nuhv7&OY~pMV%Ys&q)$ zJz8muJQ;H+2Db^_0|cd*Lk&6q0#d-nlSlRnhT$9t5w`3x#0--36p1f_9DGK&Lpi`> zjj@Qnie<(so27&HCUm*c?&z*KXWMeyIwXR_Mvk;RKXmRI7){y{W7I8i$fkp(%tv(O z_OnT=-+$+Ax8HW}y&L;@>OX^V(dMqaUi*z#&OGtm@4flj>;9Y_M@Wt!2EIz2lX0Gu zDN%?YvRo39tRB0l}l?gQdNVYnUmb4Db z3iThQ%F#5Uy0hd^jATz|c`>x9u9$Hc(b`NKXtJFo=^5x4s3-$O076V-gwTQf01V`z zi%@X@K?y~sTXjpLzH>521kzb55FF#ezc!T)x=Jq4Wd!@EQYT9dmj$$|O8T0w^e5FQ zcV&l(>UHV^1M+p07DSkoXtf7so=IRk7TXY>XeouLl`$G;TxS-ch+N^c=VBn{J7>|6 z_C+^{u}{s&qReFjJz{g3nM3fIHxtEn;YTkqFM9F=Br3D$WQ&7Z?xL|5a*Q9Lh3@qs zE0eK8w3h0c`vk1k90>u|F4XBtzHgvim|`iZNAC2e4r7}SA(#^MApq8x!h1YRIb0-k zueg&0Y*-pt;Z(zw=c-1>qnv%Tv&lclBh_9!U~r-xVnM@}0czf0=*83QF`s13tk2P3 z)QZ*9Bs?9%0HMb7B6-l6jUvlWg{K?(U1{(R4T6UY*jFgs^(p-gWqj_%0Q@K!-fxaO zq>MKluT_b3jg>c@C8{-_31!P95JPksVPzM#G=x$kh$eJ`C;ek{aHKU21;vMh3d!s- zaCncJ3=BdZt0E#+h9}xW=M{~fZQ=Tc;q5pg-E}(M>CWy`4}P!LAKZM` zUEOYXIvLNoW4qDZK6usb?|VP@v_AjX@yTe+kG$eDmLkHXnv7|nYP$SkySgaHB2QtZ zT+1>p9pgAf7NHUk2@xqe8*cLL6A1uR!XuWT0FlMKpit{LXLJAC?QI_72y~FiMB~C%Oq~5!wvKHW@D>8 z-s%0$pDsT32ZI-0nKk#cwl=yb@!JvT=9ryZpO`zCU!WEnlpL$`9&0(V-C9n0F4E<{ zaqsdc|H^iMGG2~b(`moQ22Y~37ZVvvYen)!VxC;oXpP~P=Yq26Z#73#zcLS>9{WNA z#B#{m=*;>0G|vW|H~QmI_xTrBUwN|m_2Z5EpINRlSQCzIxcV~}ZcXV%*p&CZYZ zpq|Sg?bdK>WAVcI#UI^2e&O75aA3ncAk8QxAL*samhI;|V3Bl@I#ibofY7Xh9iG z$Xe`$;B~V!K~OmsN%+cC#*BvRk?nMrC~}Yi1|5koa{*d0)h~vV5nM);_!>syLYP=Q zR&*#RJ;@{HY9{5n%-c;sm7@s@2viv0VW;`oxLAQ8`7nZM>4a;V(3aS5h4YP%M|Ew^ zOuQZmGzz~CWUng#LC5*91n$Y(vvuR$cfaesKQJ6_dBz0Nh}2H+^0_xy{lE6&%S4J# zRsdpItvPWdT8lLbw5?)|Ev_8LGb~l~qFC#6i)_g)fSBsaV%T6l(8syhD}N>$siCbz zVxfC106_qY7BPG^*LKW!;R=9pTcjH{;uDmqOnmNu2GijVnTDjRKMnE)xtSX)P?8mM z4~JC=NiWKnVL*U5qfKo3(-R#|-iXRgBwFDbS&a~_QnnL&z>B}GoN96XhH9Oeuon%T z?O;>SjZk8IsthE>eKpb$nBxu+f>bClmX)oxW+WIa^x+ta^V}GYFr(V_m4cAV3t^VA zg$-=v-ZwH`TBE#qDj~W%yka;`wBR4(ahY_kKL{yDtf}QsOw6{Y`*aGu5n&_|zy9l5%D@kNJy0>rr z1%g*?G^dvyNvJrA%>=UHVwz5NwVll*Su`{=%B}#UQs^R7oj8&w^8jHr=nBpRKLVWOE6JqmY} zdjx~%^j3(i`AENDge!)U(?=``KDKPfcHsu6LCTASlQf{A3PE-xN~!wUbJBD}3K9-# zwOUAIspKx?+8;wS+IUZ=s-UnBS`P4vG#B3!m&f_1*73_3-i0<%`{3v$tY-g(8Nbvv=_t;JQ|ZeT7&@o>_rU zIgPE7EZ3|?9bav(6?QINnBR0{`oDf<|BXj3|MnlZ|I?=?=PtE| z`!-uVn}>BdDUz<~&H{++m}#(1To|QbA##(@Ogdd2)IaSlE>C~rp3%Smh1PWkxKU?0 z9B>B>3v#BxFpX}^^t1#yXOdXL$w=cJx7Mm6ftM2sJiKaWw{dQ_$rFvpDZ+rt+RnjEC? z72L~QFq`>J3`pk2yOF<+7b`i%Dq_w6kdoRfes0)9nz1L#YREf;btaKeqlja3#NY;_ zB@sfck(70?6I$ZJQQ}&{;VpU=6iU>qF~&O=b45X`EUzHS86rLIZh|^fk`OHwPE6m4;*N?7*13qW(aldB?XiLjxUr65&P}0Wg^Vzl-8gXq;@epqti^L z3Oaf)!tt?(6tq?=5^J2$$te{YIPwD@a2%fuI4}hd6AR;!=1vYK^NbqP5p2s~fHPmu zy@N%l0-+p5_E8oK67n2tZodY{16g?j0;753z`mQ```&lnc1O40pN_}2<<~hjy1N(O zJpQfkJom_>93^!4Wj!2}aS9x85bbe1rZ}uTnTuLEm0H~`0#DEAS52t%W57TT{N%{W z=+~)u%Y!YQW3bp>OH&XtEOI#sb~_{~P;Y^$mg+2#k{P8`3PTno0!x|!MXWl^JYSjY zK{nGjb}&pl+G9@xIqPG$oQh0|4OOh|BFvpc=&+43U@=_K1iUyOID`*0V6C2DuMAg$ zj8Tb9+IU$MDdN&zWP>E4)TJeMpiue3^v^eWic%tko=%v;fD9+@_1)_!)IcLUKC1Yl z#5klYNeT6~LmK=3S2!;;(>j^T3C|O|P5@LoQATDOYMBmm-9%kDur^UE^?Wd54QFIo z=6@+~ZruEf-VByhL(f!)1*63npt~7O|E}TTtgI}kQz#86p|6b;OWYCV$r0FYE|3G= zs4QKQqY@nkf^;eVvDcx2V!)>dJyDvgiHQ)F6!b5GxqQ0paxy7**qefg^t>CSp26b0 zg?wR?LePV|*^8f?o>7w#Qb8h4O_3q92n`VwD%gzsG(j$Po2}T4O_8eetwewbr7t!V zDC3G6eM~J#xj#{{8mQ-ElNInrC5lTnVzL>}1Vv0j6Ex-M%K$A_6`y}7LujJPjO~#~ z5R1#x+GdUliU1I``M_vni?&vR3xeWN(SaTe;G4NTFcHK~+si|S60NicZ0KiE+yK6T=a*Irpo`E^&aFyBh-Wg?NPI{&P( zg3@9Eu&s9hZv18^cQaCCNd}q0M>I2$Yp4u7ki2A}QIoR|0(F3=P*c@^WiaC} z)2QQw9`OZ0oo_0w+FI=TN6l*ss-eS?20&N%6)D1k9f|Ru>J*-7NLflrFJK#ObWhh< zp#s~W&!AI)L*3Z5VC&w8Hbk$zG1w|9?{h5w`_J=kH-R0-MdFkwh z!Qdc2Cyl)>4#14l9PUP{U%=NLteTBg~pngbGdWxO|y3$UfzDq z>e$uoL%r4By^TFv^TB{~o}E5xFinF4CRDyN+?pYixsM+tShg;Yn-?$jFN~Jw$Bk#t zOrCqK@%qKVsn}_V6pAe8D>U(`u+lm-RP;$nVNYyc>55-n=Kw2fv_VYy?zrnK_R+2njC{T9A50<2JZzZ~ za1);yR7E91@~_DgW_gAcd2svCfy3`OcJ%sVhp)Yst5@^M)a2CUu(Chi-Ff0W-#Piv zLp)x&$8YAL*sAD`mj$6^y4KlC7w}DYZ^Ys9eUq=cK$Nw-WxDau2Ouu=GiHTKqO{vocwf&+NGDF*?XWB#Fu-LigF-CDJq1u%|c*XYqU?Iw#d7%IYz zgd|op(&nvhG9P=^8JfO1Qb~Krup#d#K_1=Q4CBBiEL@FHoHG~NX=5bHk-?e`5yCKh z&^V(9d+TztGHEZl@w+Dgq6##|6%}gBgs=5s7=)fX_NK4cDSPG$R2Va6%`mN>Wr0eI zV{^QXy@*y1)>-X7)rbpjJ6>^9f}XYx3b(ZhRK~xHA)SZUY0V;)I@h;AAx>c4@h=up z#S~h(6FOl{Zf)QxB*B}#Xi753{Gu*1fCDrTr72)#MC1yP0}lxq5s2~%l1|d1=H@Mb z#|H5Wvx-Y*MZ_Cy(Fd)$8mi264m4IpBG904?Ln-SwJ$I#pN?ktC3`B|( zL(H`~`%ZKyjar%^&h9z&fXvdXG@398(vFuQP#WK&ACL;>Eeeef#}X~qxtsvJ?4&qs+N^9Qy0uwf^Ap%WT zG3Ja*KpEw_)Iq0ImB+?%FtNDCr-^{8sYm0`hLrFh>4pluHPa31>udhXtBgT2FsBWw ziAq|tuJQ;n92u%EE=UVmpCLTS0Rcra8=}-s`lBd8o0kWfdcK^bY<$%{F$mTWk|V};l+6dg-7Z?5u0E}L>iWvru#5EK~- zr3d_&s>7+Pi*Mz^EQ8HVEW+quMB7QiLmvG^r02>ba)h7R<;#Xt1cQgMw0X##N4g-y_p%B#X3C^e z98#)`H$kJ9wp43`QLgzKEmA)9gm*i|1@xt%6_#65I+R1{nimF#3pYbi1W8`nTv<>j zAuf_U&_E&LBqO8ByToPN4|GzB7U2U+hVTgusZ{_{5YI7uHB&FrxLydO+Ce*Oj2xVa zBm)Fq!vPp#iO{QAIV34YjR@Az$O$UrK|GQ{vCbyaMW<+4LGPLtD!n#J6U38!6<3yl zGRNL_N^ARQnlHq%$)qh#G^NZyRwZ09B~fD*=BUvrj#i`H22Z!)-Z736`Icj^?+=iW zjg=*{@DMqem-GWASV3iqLn9&QSizVMpgH_nOlFI*Uu@;ONE;iotM)Ya?QI{}Ty72< z!(MZ{yX?6fnh$u=Xrp~$x4AoB?ehCe)79>zb(t@&PnVactxG%Y3**&f;<27K^xiHQQT<4qbD@jn~|G!=6J| zwR&Be@@o~ikuO^fx_zDub>e~Vop|W`lS@0?NC{FFg&59xJtqkSfutOQsaa&^ut+lN zwt`wj<{VZn>tutrW*fa1R}jQ{LUJQMay`n@3y$h9@ef_DR7Pz*JrEdZ%kr81I7<4r>%L&)L5f|qfT_8~>I zbnfO zL!z3G4D+>v)B`=S!337TtTkQHWat<#084@6(~}zXKpn#2SA1?e_H3@t145Q9c3*-l z7EpmwTUd#9+ag5`T2}FIXZ>Sd3#tlPMcP)6rb?9#P714BSkzb?89^aZ0>(v}`vpu5 zNKyft4n0AM#Sjk0YVE}ls~fWd*3cxau3`A}eT zg-z3FJ&GM{)Fze$QaFX{vKiZC$Fq&*peT(>K_P)PiD!@)ovB1N!Wjz`_!xUT2*#6= zlqM#siSN3qfFnBl@Z~=iA6gN=0FJ%77fkgPx>*3Z=&)MshIv_*c$uT!jv3EjW#mJ! z01_`|4+znXUS@JWr#B7IRG(%84JR=5tm`p{kZsa68{nxX7AZjlYd0t+z?6#d@^^+v8U2XwVD3nn{gg#1>Utl@_6r2D- zV#x4NFmWpzHZ6w|Yv@yw0up%i7lWiwkOLO1jMywwynkJ(GN=?9(T@o%Tqr5XLao=IT3rBuvUsNEGnd&e^VHn9>VCIsy?ZHP`93}S;O z%-Sz8trV$*ArEe8Sjr6CD^_a}Rgp<^i;>DpW=l<-2TBF)9CEf1Npr?GHhAoRZ-Nwh zV3QG>9@7!eN_R~*4W?U}cF%fWxY`vd@;p?C z-B@9F*zmwZxa38HC`9M7g&}h##0wA+VK`-ENUa$3O$~yi1Ywizk>Uvf@D+y+ zIx;pGi~3VJ+N6Vxc2}HP`^F`aDEl%=L-`h-sC2fw%0Yrcv`$%pX|A#ffJCQjDMz`5 zVUxZ&myQ|mF!OsJiZpTsYL-EdJjFmo6Y_rDL7S0V!%yZ4p<|xGhAYvfisFH7pz!S- z&*JDpc&4)>#=cTOx(h(_X|>Q9>T#q=k*vzPg9YVCprzCaiU|P>s7GO>_=P+UQ6A6T!|f zY-62Ngc{}9&9WNncJX)#Gzg)Yl7iZX2%@3O6v07knGaJmw=s}}7CMLwQa}pCs1Q$# z2-HozM|UMgN3tz@HlR4-*KH{xan*X4DqSoNBhcPOD%*c1` zE&vA+nAwUY6w;pf!3SLFl2>7fS;PrqKZD7z3UA7%#KU`rkT|eRnWPB}=d`OONDBuD zK`1RP4K&+Us#QhePXbLYZ^xzAiQU8vAdN@!{46;)6!Zsm}H(5Eg~ zLA_Hc1(Hy13umH2K*E(8Tw_l)v?li3H;g68QymcM^J{a4hjIZ89eGXPl15G z(Msc*`La^9z9fdQ^gw~yRBzbThjFQ9oPi+yY;>he(u64M+UeS)Wcra@$XdX&fz)dZ zA4DUoegEi!6noTeu8Ei#d19onDJD=5qA37T}+8eFzR<|?kwKqELJ~z?x;Lic~;J5n3V5>81_Xb^_ zL)sbidc%2t&}em=J&sDasNe|p*f}faJj%f1D0YkKn!&V90Q_B|tVKI0(8XsYu$ zFK7}OB~k_L%kI_=yXvPTul!zz%1CLMfDmkD-kDF^M81G5q z<)?rR4)#N4uA6(1Q_6LS>Kr67NH3QPk;5`733{*@FBas9{)_qMzI`{}b@#3BfB$ti z-?F)PFGs3#u2Zo}Vwlpi#kb9QG~t2=Bs~7W1EWh92|eUt(K?ezu@VW55a0(4yJ!Z3 zy31@j$U{aCpZWz3{7cgJiZTr?uq9G;Pdq~>KIyHuf8Lq9}L!jUyN>MY1fW!qB7CZzl#UV;d%f9Mjb^@JB~odm_dJDhkRptp&_b ztWRutF2YyRASRWhfus>X-nGCsAZ%giK;@=AI~C$L3z9L^s2opgwhhuU;5dqU-QP!4tyX>X~aHR#PW$}K@b{HCFI zK2RK-ya6g&WFk7VA3HLZPy$b?{cwSyA)P6eY(Owi{g!jXg2g)SLkc}&ZgeKkMFqQ= z!tjM1+wotD-O`clAgEmB52`?r45J$RN*M*bVSjk!u)Dvpy2{Sn>E+hggy&DqdG5OQ znd$bU(f0J>{NmX&?WxIwy~F*3eWK!#k>|jWbw{=Qsn)nScCd{N%-0GY8qM0w9gVnX z^@JB@%n&_?WZ6Sr?`WgyX7)?IR~Ym}5ob<3tu-J8CcIeG>u9J8VmGj=2npfDItcJW?_Zc-c&!_>&kJq#|d~+zn4ER24gv zDUKSpZE8tE>`@)t4`rm*4$)_KDc(}ba z(&mx>%#`{m3Z}%eWolqF83zoFWmiyqM-Ak{@`is-XtUKEu7_T|v^lf8%P#vRt64y^ z%gPxX@mP&;5=A1ew7?$FM4q%PedSWZ{$(hOl-82G?xf5hi#%@Ba z!w8y6L2ZI_-oaj%Rh_hzJJR7qj|sMP$1PPW5j}Pd_4qFX148K}$T?4G(_}R=sWJuF zW5Ex;WJLh#f}F?*^~?~k;5OErj|9Y~BNq(~hK-dFZ6eNxv?GcQwKR=f3$ai1)A!`T z8aZs*36DY~oPA}iUY2A_JHcwYKr|DQt10y~p-2QX69iH@j9Omi1ISyaZ)t+o0yYx3 z0W9*{?8Jlv3B|SwK+^|Y(uJ0|gh1{s%BY|A%q`XfoKC1T3Y3C#L1=>aP{#(nLl<0D z5^TMbuSQ5Iu#{(}2fFAr=y%i$Z456#13>ZM78;r;+N*(#5_pv_LoA(kVA|OrIVKJi z>!UF;6WOBZM{gsV4hqv8DBzvRg+vMyKwHBd6|5UDqAj$socEZ=CoF(XBq&xQ&Y4!2 z%TQoVWq?^t`Z+@ArJ_2m8DmAEUWX0?C+ZAJiYP#hUMAAY=?ebgG^%n;SwIpS^{F5q zaUAVLL7!76ODbUx;?R^$3OrJxFes5M6~Mu`yU8h>vRfs0- zlQCrj99KJaXw3QMRba%R2liHnm4vaazh!a#!Ij?L}Annedv>6r<+fI-r8BJ+2HKP~;_-`Z6AKE>1U3v?ToQt^b^UO*CQ{6*_}90%0XeP@{K1W3fl`jADp7y}%(h z)L^2F-rRU{=#@Zxp$ z^(42P`bRktrr1QT=kPkE+*F-l8WtF5I|i=IXlLQU#XU z)NPfs!5xU~_w9tPLsYr&w>2z|mdi zLnfsxj7j`>2AU!?6q;=*P*&LaPZ1uW%aSGui~(h)Jos7aFy7et9AIz+wPMgG{upzT zB_sxV;m)A@MiA@@k!n(&Hb&VV?&?>1zytynC4RVP>{JdQF;ZLt=yW%BDPyUTt{(UX zlnkJ^r71S$LkzA+?JfM*9En1uNfFw9Ox2*G0b>Dmrkd^5AXnY2U=1c}YmI>-Tt*m$nlT?ysbvx^k`2RfC`d8jfDv`%YY4QvHrfRw z-wN|WzLGRCP^apNW2J~%5lworB{DqGQpTd1)G!$T5%U~U=41585{##pP^^9M8wP{R z%PBgkM;%eMiN?_F+8vq13yaZ}SWyY|gqxN*bAXiAZK@|yr5izQtjuy$Tf(+ki4r3w zA_TO1e9b@ z&aW34;kXhhT8jgyLUQydA`!!o$>tWNfHEI-I0F^K0yO^D5QBnGGqn80hAMcu7>uVuDr#Svp*UtNgOGT@CcK1! zB$tDzu3{k#WW+pi__ow>0P_sGc97#Co)LNM7ofT8iS;KxcgphG`=9Ks5^QVwnn@&+ z#=zrvV_or8x%`8%F(q6vrAZ<}1)df!yz7qmEcG3|E}2qfceDimk{&0X$1UVXK{x3?abp#<0));6*k(ed?6E`K5x5Bnm z3~04uYm9A*jSYf)#Op>I6O!X_QLj2$ zIY#Kc)n*6nu-mBDNRa8SfM}_mXn+YqmTn?N(UQI$qgHa#QCx;iZm$tE}bU&4VZXM4RYI5^?n4q#_@oT;lDw@y2*(5a?#4zAsHuhIVKW2 zl1|bZ_bD&Hq64pO`JiMCsT`yJoJ-?|EP!l(BcG0hjwh>b*h=)}ivZ~(W|Ss$lN?Ck4WlaXN-g%gygOb~ zZm>noPB@K6>`;Xf%lRMQYKL+IsWSP6B|nN<1<_?kun84xnAahxMpOHc{Gb;V@#FtB z#z|L#_EoVdlg1hicF=>Hc51bq+g9nSKxyL0ns#YrQ9MC93pe!}VOoae%w*LRWQtgg zwvZWDahL}D;tkfxD6k=t^@EnAQ$*3|Z0=Yo^uTZ#!w$x(9v4+ciP6w56%dB7t~6X) ztdu;N>lL9m8O7GoF1JHFjhF=#NIZKSI~eoW#A?D}r0l7*5{RGv$J-QYML%q*D%(a@ z3AI`$t=21STR?{|wk?Q4y^0Ek>hx2ytI-R9gsHq&oHoUSHqaq#HVcb3v}p~=V^gv6 zYMSyv9@KFgiYTVaH^;5~S%6Q9@CpafQLckON#au`CA!-<@Q#7hhg2;2yo|5>eyx48%q$a^p0!&K1 zzf?;=GMB@Y{@H*lj-a(d6>x#ianjh^pt_pnNb$!@*v|RVfEYq9Rur__@MQjy5tZ>$ zz7VE= zXv736*fK2s51~$;=ypxbU>YSt+pW076-D3UGLsQ(~J>9zT6>Z+qqG-uhavw?BIHoL>YR;f8Fo&qct1 zCuH-rfWY2Us@P;vl&4gNIsk85jHxv1A=XGm~nKPBIllys-gz(2(m{ zj7o9T@a9E?xM`Y8T@k38VNy0!&KWW)Asj_+R!)eC0FhSq_fV)k`S$pkGbjXsXTf$_ zL#!3Ka=Lf-{#h}CR$b`6YK7&3Is8iY{=qXDd*Kuxy|TQo+> zku$3&bC{;l?Q%t%sw}AN2L;quxu`^s7|}pUbqb^$_hPI$@1Pw`Q{`w&JHaSohqf;7 zs`G%#$5p5mX}!XQ_+>9~1hIlG{1TFfav&<0`l6wXWQs|E!j$@ohZ?q}#ld8_*whFV z$6^(Tw1}lm;!O8+ECMc^YD;QUqv}!|%~s<>%r@SJ0W}JTDn}MSB)kyV!PS_&A*FJ4 z8ir*jy6g0>c^DGn33fedQpR*d8$KxQKp;nIw^|96^HQ76%0aT!cmPpAuD`^xHx=U# zuK5F4M|`LH$lGH?r0$Wiu}*uOhu#cv7~nwXLMz9;fIxXHD{0>n3fA2$JPD%+9OOs^ z!6lIc#?%ua41vo)2;Fo`jv?%d!@(346;#B^rQ~nZnn=;DumTvMgVN+cQVQyYh9jOQ zb$mQ~X8G2;?_R%iyEQg8;CZ(c#Vo(@gtS2Q3K`%rLQ23g4#qp2e9kXU$c0hi?luQVeGFsJ z>5E;n1I}=Ys00k2%i`rNIQa`NMlRAwnU?3={^n!%$Y{$l#ne-&_XLJ7bOji zuRf{IecA?~7de2!wlMUN1rFM38=9NZuYlNATR=-t(E%^2P6VAL6)0xx9BwOF;Z2^4 zEqe8~fht@Y8dVesTvtSB%t9EV9tUY&2XxVMgxZY0^9$D zVBam5Or?iMrltFuM(DsZC3y2xy%$+1vVkCNnki0-z1g5NC#k5XH<1P~Now-M8~Z*D zMVHQ)wL^l+`2}1toY7iR)--Nea1zKg(0n3hGvR62a1m956M%-9N)UsZTlmQGP;0sW z(V#rtnk9$dp{d)BQ<-(VwYqOfnKP?UXx^wxGN?#3A0~`lHmXe-wuF@>fdPu=CInaz z@^o17Ni+TxSlH91`J%E&yQwn%@zNE!woW~6E8t~xuiomZ&9u%g55wU9(J|BENUPHx z85^0Jo>_k3{P}Cwmd?EhY!?O|=sn{*aK!W5FYn&{!w0)h*Z6^9hZcu5wDAvM0&2Vr zW^8B1)R1d8R_vKeL18>|F_RA}kuX2$*≀+%SRgCr8ZSfAdYVqRl?PrX3^)xm+m;1^iY>*y zjo}iKaFMSWq(Id;l9_{1mNJ;Jvn6EUuz#5h>K1p*@|F_8Xi3$fP>@BP`&gD`3fYNM zx@>42Q|f?NpG41i?PHhVMLsJL^+AdpT|EG%ilv$s%5y=&o-XikR~G!70{7>fTdhuO zoTL1ishOFv&e%ws)bDTr!2K}mPo6xu`(?M=ZSJI{afL?IyzC8?j3LmBk!HnkR-e2P zk)Yua*Oci`S)iIF&THy$v@|)m50!nEC-_=S(8%PJJ-ZjR>XQ$NR1-Pu+{2#d?hHDc zriil4XdbU}-~Zg1r8_@+@4`!0dEj}!*H^^fK^#TZ{^r)BZ@;WOCMHI z=`gGpp|G+i-vkhhCbO6p7RHeF>2b(v9=Ad>QF8WXngui^9KMK}Hi+OQ3p2`924Vu` zn{}oTBsLw;c=3;Vk0bc0Xa^ZdlNuYDuAoS)Wk*3z2Gd`G%})_f#DIVj2NGCAnJ>*! z%y>eEkWB~!AV(!C)DZ$)3v*%ey=Z=aMM=>V4VaDtW`lJ3bq8u_jRU+}h6qb$f1TTs zL>H};=i;`5mB$oeARR|4xZIboMA*PQ5-%j3ghB3LG=g$Q13fc8Ab=_wP}KY-jLA15 z!Ff^cz0VLSGDWGkW5^6@B)ed%!7OOh0H5kwxY|x>OzM(pXn&Vml?9ysQtk7X>~;kX zU7;cECDq5s8<&i8tYX;;RGoul@;ECn!zDXLN<y^m;yT%y@v~OncPf(Xz|@vbPC^) z0wV7e^IwyA0aP`n2~`-tt{%{m|F%-z09uT^&Ge9c(6|OE&!~$zrw_IiREzyVSEPj2 zDzQ-AYSv5`ERD&OD%NJSH|$Qlls9$Rz* zR!3wm=94AZNbpqUH0E5Q1386`tEOvtEU$1)fCB*){MUa5B+CL3*!Vz!jqF?c#+`0j z)kmG62(?7ok&qP}5?wo>@3)ipt2vokF7eaUf($S!Q)IZwa z+1c6H*j`)Tc(Ss;xz_FWK){cpac@9y6Dp~w4sOsZ<`q&Mre4b|sRB5-S(=E$_)%yW zdb9godG8i+42_3V{Kuoz! zC$f?m$F_;OJ&gERQtE7uIp#3EF?IF)C~2|zp|#SLl>Gi`(%Id9|q!HNTbaM2T*kPf>7 zA!IVCeMKd^ zoTKtSBs-pAR8+b!h8@;j>A~Fc>6`EU{K88w(KS8hL8DF%KkxM) z{&4??&pvy6|A(VPZf+zfxKYZYJ5%EBc#la33Qky|8lHn za|0|NIgMaeB*XMW4Wt6(88|EmHrZ$_DX_y+W&5}cG|%g)k~jDC-8$oGWST6aeNH)e z9uLlxX&9;=m!q)oBwrFtKrzQFgAb;qT~q>5CR4o}8~PleAYQa2<~E^(N^y|Ws00)! ztD~Ei{CDU|p4N|_>pC3io$tmkR@h)`q53@wQp ztxF=rvaNU!`hi`#iF1KZJ25W;$&*mVDR4=hGJ#{bpN>Wn(T4FH==!41NeEYC6tZtK zU4;;i!_jO^T{Kx9w+w+NFj%)Zy4r#-0pGC>b!n~kKw^)iv&Qy{kgL%`fg^zB$xCac zJLy5WfxuP69^a_c)g(FaBCBQ_C-*Vs$@4HvJ$~7=QSKCFmO&e?Qs^kqP>Uhc4*R+< zRvP*lbQEd~<(Ma!IcN%`NKj`>{$)$**hEj67a^dWK20FG!fT{X(n}YKn61zh6UZW< z46kNtl;?6_Ri`kuy7+VF?>jkOZ)q6@W=6H%`(b z4D8rK{5qz}0OSp2VvtQfDc2Aim`9f`l?pawSJi@}op1b;2V-u2x@()LWQ~Cu3#{>B zL1I82TPTUQmAWwo%ZXmT5uLWBEpBNf#`5M>HK~wB-t75o@B$nWmLTUc3S`Gj@Pllr zd4U2Qu>q6-QMjh1%GIi#P#kBiSCcVD4}PQouOKQ}C{WuOKe7~aKrl(tKQyC+;{+QB z#;uSxl zRy_txL5Cu>l|qU#LYUR0VE<@pVg99Cw^{gb2GmH|#)H1mc4uOIY-VA8;q>y-g>#D+ z&dn??a$=Nwg^&4p*goecwd;^A|O7tPmkX0K(Ca z1jz{i%ST5_w!FxQ`*JHyC^Iwl*FyeL7yu{~KScs7?vpT8n4?s2CK+Vfr9jZjqv<0{ zk|)L0$Sz_UrKE;Rf*%uFYK9QZBT%u|wqEiUglsEW?~eOA5Kk~<^peI2DVh_20znO8 z8i{+&rn*2Bj=~R>vWV-Z2*$}8=5AREswfNNXe-YIV~h}?+j+4vm;mhBDfb4@AN4#@ zwK2GIXCTc^@PGqL$x)=6C6tLCu0)e<2@m3|5sQDJ)oj3)X{S!4-C^b8;f1uA8|H{W zMeCrc6=dSZkE;qa9ge*U}1>VIUcHFNsR#cNlu z+`Mu9&Yc&ozr1|@+~mT-#MtC8zXmeG$i-m-7YL3xKyJ0$`#W134<9;|jMl9jm4B)Q zp!D!^lmtMWjF~qYz8Gqd2)Wyiq#?#er{`{vefB(_OgU7f# z+UX1rQO+;AS6-#$R(*fmoYbteVwgxg8}=(lBN~l8z_8Krpm)Z~>bZl0ze{VXrh$5-jezO%pT= z#H<5rE~TJJe~kq>l*KH)9lH7FfK=|%eUx%4(XyIB$fq5`86eX*y9R;}YB1;<;fJ+a zrx&i2I|!-MymzEgkbOsMIEXjevyy-4bk+-O-ScQD{2-ujG)Is>9cC-fbIbt(IIVOU z6Eb#0P5_6s=|mlO+)Ex{x}mbf2wq$6x%!20Z+h($uoIL8>#_d*_u-BA?^@hU`G zL^B(S8Th;rO^d>RxxoU;fm)IyWxkOTUZN6!tx-r)W+cP&=m|fXicRflK`p6fTc)Uv zQdF7e5?5h%gFi=l^GrnXE8}pGf=2J%MyY2g5Qs8Y&W@DdY9wBHS>!4gkxWy4 zA*5L>6j%V@uew#2J;&U*swpo%uY;EyOT51Q#>bIoGuEuaz49wn(qa!uRQ5@d!xBvLY-iAp+2(|Dag$@p5%=^;)4{x zT&UR6+>lKFtg^mvmojUlD?>;smC}uLcZADyJkeFmu&tZG(oGrlbTYR&pT2nU>dhOp zt|oQw70R<<=LsHq(`qHo|m8Wy99O zQXHu6VOu91X_gL*nksx?ojo0n9qKBkks+&P!4(Qx9lJS=%5G7EBL92BLm;Y0B_(R3 z0C~qXn>vW=vRsyVtpp8#Y{iZ|H;5e;Dao*?SS1EcG*!WtI5n%<4H2j(;{==OAWUTl zkeMFCphh-4^RJwZwQRp;<{=vrc|h2)aYJLE)e#`j0C`-?Q3f*2z=tHgLcBD|KQJ)7|PJIcn zUb}kd)}>coTReYpY+{Na=T1Ma$D$eaa_%uZNS{9+8yn}2`PKXP`aL>3H^hqvczGrD zpgq%&t{N6U`8O9i$^Js9!JC}qY+7zxdjiwNXjV#suj9A@MN8C#fv zMu2VqgNrl0*=C?1V~z&h&g{(1x8J_<+70f`$b&vl@oQh4iG1|kH=q3OcMtCVrQ1Dd zwYkx+l~~6z{wI^-0I3ScAw7;Km#~rbl@X#H=P{cRS*P{b#i4PQDdVV{hMjc+#ZFa* z&Iz6t%7h)+Nc162g%WeZBp?*lpyj5Zz<$iya6uJJ$d{R~gkt>X_?RGz$28+VY=%fg z=^T-wfIT)5ryZ&@w$Y&aoIW8f+%(07C^A-hOPw|ymYg>AO$N$VpnGQ!LJT)JC(3G) zB{g86v6G$*eTcn2;20vl5lcb2-PNoW?!~U4D+LDuT8L_l~2FX@0QBbCfzK_ zjk>HS(Gtym=qmoQDUkG0J!UsCsc7dbXLDuAkIi(TWPxin0yW1_&5 z7;N|xt|DiQ7Ern!A8_ul5~xL;O8xJAM2YrdBHL8*DIF;CjRDzJ9X<;I|Lty!aMBWC z-@Ta=OS!YuChCk<&D1}g$lWH&Io2a1@P-ugR@yZpAl|Ky6pL>D{C=9nCoJ*Q0Z2s!*h|bXnX$pJnpT{K$Vpu1#};jF2sJ-NZ9PkSlBs zU8?54`qLg-8$32dkX>VmRNUY_{y`>G?WQMj1wPrG}&d-KWy;Fn84DYrjqA*j8-Bn3$dBN z6i+#?Y`nsMQL7NT5_*logQ~mT{psb?w}0{8wL7o3r>6F{cMf)TP0MFT{^L=;T(=tV z(+VS#(=+oIE?mC(+RJae`Qpu6%P+n-IX!o3)E+VD_Za(FAA1up%N2}pcG|;W40#y9 zl`*(qd2k=)ej}#Fm8=d8sxz*l$qhbgmaZc%b4M)(7Qp<4WGOMl489YoRUU9T*U7ml z*xa9;K&iqI7;g$B8~P9>Su>Z0WE071gk>Mc=&djR_1j$HI5jdl81zxoZnuf~4`1E= z_%|PHJ$^#Ajka1a?X5BBr+@P1twvl>3UX>3S4asnK3?jhIp}arIr_*-`;qP>Y`Uf+ zyd0&(KI(VKNElMV0P2C|l{qpXF{XkUQ=T&HGbW+oqzYqM@(RZUaWH2am$ijf3TrR6 z^~FPLU$9Bnqu4NkO;ZTVBSpLm(>ffPV22h1i&9>oLJ%h)PyU10LSgHECaNeZ(6%OeSaz88 zUtFfY>#_NzmpG(&G6{K$5DPdg7);G0pqHgWUm}&!7{I|U6eU5Q-j5FRQ4`Uq(&R`= za;Al=&N3n~m=Xg2(^!M-ATKQvVXc{F5+zW~Arj1FVJq1%H&NvZ03UI%B#=)z(&&tJ zv&2HBJlRJpUosKCJUg2~R^?SiYu6U`ao1j&pu;Ag0Vy)T8W%Q2Aj~Erz?VdMpe1$% zXddyQI41ET%1sUWvA-`gGPZD=hHOY9tcXLqdLM0Avr4@JkoOvkm6X{yjGL#en3PiZ{{D#JH#*BQyoT;(j~)YAUN*Q z9JoLtF#icUA?75ad{c%Z(~fcp0)iAMoc3jq9Rj03*-vROQ;1m|(0{2#3ZQ&s8b*kg zma4t{jJ$*zbVnuQHbO)k9O74$#J&k5>99fH^)u$=)9zslKS>qA8|AQ7NluNv=$&Tt zlv;UcaGIVP48^IlkE+42E2uOZB#)jTCl&T@-gL^n$YhmG_rewp%s#!9e*n0;23ixq zY!D$#U>phgGD@gx*ucL6;^3qHIIcU!rUng()jo}4$r|<`-f~^!e7Q5K&C)(0(V&oZ zE(8H9cH>ET1E6$eaWzJaU%=s^4eJdTut+Qzx|w0D(3@giCyFW^fDF#@neEy(P*Kf? z2R|Bu4OFq|AKLiJH=p}kn}>({a|??T)6;${j_L8}xHaCPge&Y@YpdMn3i-BiJr(Ps zLr^vLckwhX`(eOA?KB@M!AncA3dwx3mULDk4Un9e=fsoX37`1tyFNo*D8otijA{-KbNZ0x66BlHgW4p{W|HO`B>Q2~ zAQzKTWgky4w9j!v&5So`Tx-ZISmSu*t$+g>A-q@%r_vckW!hbNk};t5frf!>ti+gyYcQ`Jm@I zeT0+Z?atVEo9rK=Y`_H;z8;G${b!r|e%hm(t1DY8D}=~&)w_^Ra!5$R2Q={|g~1T^ zGd;^u8sffrV5BIDSCr_H%YF8(Q#rATnev_lgGl710sv0=nnawWO*2e4Q56m23QzZM zwXePP#;b3<$^A}8y&hT1%}iMO;qI58{_eLstLvQir%xg;(fk>BwDA2d5F~!8&O9R* zSR~{=q{C%6?Zj+MY?Zd=SU4cYbPiBrAZ`&AHt;k31d9%6n!qv?C%A0V0`KHkBCSk` zLY7tw+p5MRlIl%T;1X0yTtb5tAQcl$I3(Jb{}=;vGS}Fa>*fsR>>SACAf*)Gweazr zXsT7@#+@<@=(lBl&@v~K#GQ2nG4mNyViJ<`{nJl%ni-TuPpU)$P6^GO{nWVZ z&@#U%O|foMB%Z*Aszj?`H4bE*7#`zt(BZDi8z@>~%>oq#MBpGX@@P&^F}dcy{7iK8x7#4Ac@iAmc~ zX%kAqqX}pk`__=3rTiqQKXS!4PMD>m+Qwu)9;PkYa9Sc1=7rA{>;oh4aWdtk8Smqf8i*)opO<`4yPx=X5 zH}zK^ytXY^S_?(ZxPf_W86Xtb7W0moW>T0_I^xADsx@C296Yr}bIY)Zhp2MhMTa9i zC+w27it<8F)FhYK!FQnrN_N$St2`0{@9hEQY4AE&c`DEVp&iTEb_5?~v4CS2Uw-Aa zx8IptI^FMek53(OHf?;ov$wJN@vnY$@2k7y6<@2vBd|FgKoTQ@OrVFANmR) zq^OOVRL4!3a-{^lPnE}9yZf!D~Bm(0vvFnzB{;l>((28 z`wM>fjBotnDECVYx_ghlzkj&9JJueXT%4Ppooi1_a`l5{$AD*{ds~n|;E^+_aL@Gu z?yBe=?(S`E?{D$QpnXpNkB+tGmKJB1mRU(151tLT_=U$#Z+GY3-Mim@`pM4fhNn1& zIg{$sAWVWdt@q@Jj_Nc!(uGH?AlN3y%_rkbZgGvlMCLN{@c?JJ9ow7hb(?k)POjNs zWEC+&j80x!3dtdeN{Gx&@D$n=Y*z6$TghF{0|VkmNUmgZ5X%oGJcki`LW33KV&=AK zzYQMy_+wd;sbD3(B!-8Qb_GWaodu`8rjpbSOtU1UM-3z?W}I4*<>iTSI7#g&5V?8e z;1jCwz+QzELy=!}S9@TLe>~vD-YZm{3GKXbGg!s}dq${4KMxl)` zdssv&Emjbzb7+_^uRr5vvxV7fZ@h8!<{gejxjF6BsE@|z^Q+z4TZe~-9@??uofw~- zof{vYAZrC9DLxRBjAA|Pr7xc4H8OnM?d|XE^8Bmqjn%#Nt=81UrRy(GE-aqn&Y^rU zvOO{J^&kKA(ZBusaj(mv0>gaq#sfB+0nk7gX)Xgxq7>2Fg$-?pQE0M}S1C(E+Qiz- zEbJI@vLGRjKEq=Yp~a>K83h@C^UM-+N4kx8rZGk5H}$c1GP zd#ceZI53BWhE}Hm3a$VEKmbWZK~#dIghjOQP2!|Ew7Cg%YVg+x7#MVieXUxaTJ6`a zrrkse=uTY-L`oc%OA{SNUG@>97Z)h?+G_GwEOsn}Z6+e*G$kTU=QDfiL{amV9kkno zk0Zx@4}rXugeEZMLdeyxjUkIXp>1fiMU=+HazJJUC71w>S^a7FvAh}?5dL_K4ez0l zh8ZA{#ykV!DC8@TVE<3=7zPg=P+SYQ^lsro=`x~!6VP~$5A2B%$(9MA@R~| z!;$0UNuE&T4ygbL>-mG<0*fB!SYwm0Tdp3!I=qVwdG?aMiW$r5mN|ekS0%53@b*}rq6jA6h%Iyh=ma&Z2ZTE;8a(wN#!bB+EDEF zLeG0j2KZ#tSM!b`(vdx2kWo@yQuUbzI<0cutBGGDh-EcO73ff#iw$daeKilZ;U7;G${AY|Nqs7J>?F6fq-d)nmrQ zK7edwp1_KBiHQ-{l*`fX5bg~b&ZbzyZL_#|>aQa1cvewa3#&prVL?h>}fdC?yMw=QPP5sr)A#Wfkb;Ok{ zAK@5aaR2UCEE;aV`?K-cndg1r@>`ih<8!mGy!mEte}CovJ-V7Vn#O)1!~lfgj7*Yv zuD?W-goB|GW@@HJVU zb9ax%CMV8bx;Q>P&E3pKhkFi3h8{m&`Rup9-d)?^TYy~Cn4Vvlnwy#CxhQjs6LWJM z3A9?naK)p<`HC-Jq~u4EaAS;>#q{jaML46-eutL>D|xUI$44!GX^gw8uim;nwXpER z-McH_f7?6kGB$HQRYc5Gwaf)F#6v*E$5@(OIOSvWSf~>2bKB%Xm5086?M_ul(U~M~M?we4tUh9uUTz+s8Rncsh zL&TYc0J3cwmQA2IViGolc>#_jO239EBF{e60nSmD5*$=1fT-hw9qH(}i#3kQ#>Qtj zy^YhT_i_#Q#B{rZ?jb7(Ze}0mJD=RW#w}X*SAf*Wj;OP+UF#&7VGO+Ni3f`fYkENk z^LxQLSdn%3jc3pL*yFJ+rzRKX=N6Z6X!G%t?#_;j!4}s8LJ24xuC$|O5ZHz6ADJ1@ zg*O_bO`NoYGUc5Ro(RNJ_v%M}~gqY%+E7NnV^Xq6eL;Vpq zs||XG$K2HD%83y29at`Nv2(n;xv{yjva#}Hb9HTdef@av;IMxPyrYAI8}Gi$?|uw- zSx(s17EYfYpPAWwy3gS+QFI8w&#c%9BL^)tggb6P9MQ9*Qm9bQKqo)VEQb`=IEgOK zqe|x;q7uXmThhpW*pwVEB~(^HcW5UXv@+4y=Pkty5Cd-CdH%vHue|)`TT|0>y+N13 zc(mPSBwKrU|EoX#VQY1bE)7>6%7WF=NbZ?E?TMsA2*)x2_0!&{6Cei%(0^Q9vIIcB znbk5Z;|8h2SjD%EOcRqIhs_f9!65dm*2!V)abF@v9mW}=ED2qD4I+5*_W}q!0Lc_* zqhv!dYdg0X{NO}OR93$YLcb3|tJ1^=@U>uPkxLzp5eZBLfa)iDDdz>Zdd?`4zG!1Y zo3_k$*%la*I7)7Lza8QkW8^ZVg~X~wt6b|pN}W?Bs2Vu1bJQ+Ot%kX#bNHouj{t

o5s_QhGeh1-0Zw?JitAzq- z6o<={J!_Gex8&;u&44|OYQ135oSUqgUS zA{!T(m5dFgo~}BYhXuxn31(ylfvwoB?xUi<@toBnt7k9wluj|yH@BvV6+>EGRmf%m z?U`uN$!{ia$)0ghhEEPLp0EROw^wi>EJMY`&B`4~{mrY>{?2HklR;Fx+xZ8g`*n}x z(BZ$DX$k=>;-z7etXHOt8z0mYx%qVQqSs8SRXRCXKw@&%EvRQ9u*}t#azY9>wfHZZ zRJu#-SYXEpO~mz=VFc(8(e943(D^ZUIY}ipca9HL<{tbK!gz%r+#<<%X6ATNwgd0&kTz@i?!wr(okvB?+0mF0}{AX~LKsUi=5E6c|_p8O&^UTF<)3RCP?4SDgPb zsRBpb4cLT|Z(E~n`tzCbMbAt8MY!3(ElR*=v>RiE+S$hN7P3(d+^(Ai5N*6Elkvg~ z^H`}RT7LZ zpi6qugyNP_wQLEMp(WDjFPi48fhuS^?)o}i7PRN{qX2nJI;{&jwNx(RTFS)$k|7!5 z?hz2dPunag$qxmI)cIO<;b3zpw9Q%;C22{@l4Mp%lSq46 z3Yvn0vZsc&a)}19ZRhik-qzi5!+bPA$hSx)orcf|C9>+ygR81!imPZYd~Aj@Um0(V zGJ!LB$e=UiG#1mwZ^%`nnqUKrPG_4ocdN1Yxyn+%`Tppw7>5q$qkhKk_MW^2pkf6y zo4aeJYC6y|J@X1QP|% zMv}9Ou;|!HkbN4wranB$f$DhLs-c$VH{o~AN&Zf(8KGmkniJ7D(~Bvg1q)6{Z2@4h zeHhN~@|4ETeUdqnk5+MpJX?~JiU(N*Ws+bo{2dHMvihUEPv5vdWC^7IqQA~WQ^Hw# z2?wy_sGJppA@f_xfV%G#Ohwrjm_%FZ2tbHKlbC2@ES?gI0zfwOGMe++<)=pXazp;- z|M5?M@|Qn+{pu4Yqw~$X*RMbMweSDt*T4Ji_iugX0u@!4l@ zzVW^9{@UO8TQ6R{>b|EtTUEgO&p-R|k3Rd^&%*6}?0!UAl%2DjmjUfh#_cn@1j_Kn z>EcSj$uhAvp-nu2{8LerMTfWIq%C7XRc%FGwve^4AO7U5P6en1x2qREc}3slhqaCoyH9=WJ0+6RxM z0f6JKjqBnA@&sz_PkenkC=VU0x%$|Z4)UGII$v)>;%AN(VHWW4v45V?K|tbY9|jgn zKWT5o)K^$};Lcgg%~#AA8Z)Ek!(M|yRSK_Vju4`!{@uGmfg5LrR(xy_SBbd^u99Um zVBO`9=yuGf$#PtW0lIzuoTql0lAH}gk(2ODA_g;%nucqYgjg3~a%e7)@f%~8imj3B z%cnfXHVa)7cxv!qW#z46;FB;XGlRM~YBe1eP(8d(0tPT|YOE^_E(m3AT83CcH4SXO zJE$bK&S{FtLRPRosoiJjg%&68#0tt^L6{kxmsLXRjI6RbB!WC`c?NUF1>;k)tUUEaxnX`cMQW~;y@ouimwqOl~>>Kk} zo5>T*UOlG%FyBLdZ@ECBKQA-YXp9tKVJd>_VI^m5XDF7=r+_stBmAa<+4P1|${I~Y z&Q^`&6Yg0Tc3geeN@o?cgW6Tn-f*e)>KmMU`nkotJTVZgWgX zTnBm`@*0&bC`7*$@wL3iv6Rdd;5qHKdT-Im(C)nKGL z=^4ona2*u~pUWTAi;?zfAni)Zbw)oGHmizng}2-MW`p%MvapMcRZp#$GvbozOvt2e zkouA0jJ0Pn@7bM@b#TcHq?>2JVyuTxtG4f0VEv_TtyGcPmSJ`IKz0MA3xZ7}rnR9-NUZ%;&%N~rmBv&U4OBAf`u zWb@FgJaWW!ez#qN(gY{b3W77fE1V62rsS4e#U&z{t?jwdIUC8s&ni3kE2Jer z<~k;QfR*w*Z3wr!;nsFptz1&5&W)7Q8?)lhj#M%>=d9p}U_f%bTGd(-TOK{x(EF(L zbn|Bwn<8EFS^|;SDTddQd${>kgtZ+`x>|L0%* z;h+7#f8yf=AH8yESV+fT62;SMP2CG5qO)CNZUDxZqu@HuH*ZwR-F@GH2-#V?0vEA| zlZ3%sV%Mlk($SWn7i^a@HI%-HP*KNf@-w7RH1|dxTOhg4OjVGz4pc`?vC)x>BgRHK z8iz9h=5s`@1@JUJ_OX>T~nWTv?FLwEX~@X*$46uXegQ-0U%8k0y?+Ya9#Bd3=7)|Su(V0m+9^d$y&dBWTvQ@&LV*fHP3c*s*Qg`GrQDGSnRR}6hAY4_=Hw+n zNlLEA@l(^Ck&XCO+Y&iE?UU%HqAi{aVDeFNFc=Xp23uDDtT-9bL!RLAk z)V-*6XDKUU`Yaip89vldKr!Nf>j zG*z()T>tzh*&I^#tb3$@HY*~qUYMUm8Hg<6J(id40&t8#&;#SQyFlT7001-Vdru`E zW-G5XCx(_;(7*_s9_kRL+C3$urpE!Ky@K&vY9%{U15h}UpN697NUdao4=a%ImiQLu z++-P1H)(-j>0d$X?d{}OxJexDTm^u_LT`%*K8Jj-C9~MuLYX4QABY=< zO$PM$9)I2Sns@HGfBE(1?dNYk{nk(&=iB$fap(9~uU~xQTi@xkmp}fCAHMnN zX9fG}l`rz$c_Z`h-n@O&3G+MO|6Bjl-~R{S|E<6C>hs@ zXT5;%(To0-ylBifyVlkT)M;CwBvb*JNO61IR-f0>@^(#?vLdwoOLSeug~3FaS@|a>bTrHz{0=k$ z={14UMY*POY9I|rZE)}>mBxP}Vwwr+cI%eOiyR=-3w%bG{x1${Kbs#^&+DAj)Bgd; zGX$+MVr_4;-k7A9l*iDI{n|i6&}NyOE4ks~{Qy63SB|L(a-p)c5}oPnaISoXci~(p zwTDNFi_r>odCGV|>54yG_OocYqQXHxN(CuqqQZ46^V|F86ew3x)9HvF8K)` z7x~(fgNJ02!s8`S#P$}rn)i`5R(A#zz#$BhlXo+cnyD>Yiht&}Ukpi86~=^e?0kWZ z{MNG&1TZ~P%%8=p#X}w{KWAx1T~e&f*a>E%wEh_#8A6Qq9yMY*cuKcT&hX3Ci&x+I z9g@}=*&)21EY{umOjTspC+ce%DHbi@rjgm``X zt11Q1kJF^YFPQ*3r)F+hg0GH6jl;v6kkmD@7J*Sf&(+5fko+XY0Fi)-xE8-Exhgh`pt+mcsZ3TCe z-su*eQ*B`~L&AYPRu73}b|(Oac*tCm@ONTpeNhA6yTp=Ot}+n}!_zWG0c$w~GvNzB3O*a{jj%f;?e;9$t^kp=sfq#4@O>WTm>Y%~X zm?~aE9WIDt%jL%@3bPytti8pRc;lp_WMnL2Qbm-zeS0C5O|}mS%*Z_LS=_L|Tv=UJ z>c$6XMFrqU%1kfDsfhZC+~N!3#!%85HRv3y9A_UZJr&VQCmm3=n^-SZIGTnFZ@j?P zUXLsl{H)m(WLo_-1t<`D!%SRcm`e5p5aL(V-7cCp?3r6iZWL} z`^it}JHPuozx5CPx8L~9@ArxH-c2Ez&f&@b=&%0hAOHRj{?mW__|+#( z$D?E8tp3fqX4L(}LqQtOyM0}NO?xO2(^y;#1Q z?68;vVPhGQn2Tlv_v?8ksz35I_PpN0^Y4%5Xp<5=$j+9(LMMGYqv8PA7OWAk$`a*i zE3axJtpMi8wzI0vlrvS&+@+CE*hXX)%pg(Wx{d{XQtxs5+dy?`-dXg)KdN_vsA0i| zJ&md&7v7w#TsAmB5sn~a?eV!r2M|S{cem~!dEmi9)Xj+rwtpv$Eb%n~;pKHN8tnS9 z3VG>$Ex9J4AiX8^=1gZ@Q{`i^Sy7vBHs@{^#nJ=N_cT38S7V|qU$Gw02zT8 z7>>vG;nuF41d^S;o>_PDM~SrNLD%6Sxs#Zjpd-omU+Obj(TWfau&R{MXhvBO@}A{x z#;+$LRBJ8cppIAH=`BF!Yhy@VrJ#x*XhSd1vjff{sM7M6(dp=J-=>i`l33`bTl>8_ z4OhrwBFTJp8$HV#P-@1MBeD(C45y`*QlY0Mv{c|o^(0^DJOQ)$fBy@rkj=_?rR{#`QYg50YYDdP*8$F=RCTFq7pcIbr znN2V_t03DJk7ob;$=9Z(Ip&sEb&?wqI*~1knBqNoT_Z;>*_~t&8IV-7up4e_u(rxh z6m!;aEdQ_iX1f<3`9_Qa_|nJSzv^2)D)QtOIaq=S=lLtZ@veP1bf}APEJe_Xuo47( zk*_T_p7RO~tZhc_LNQcv&^S}dilT6(r7dO@h7LiG&7L~ME|PA%h$f*#2o`JblVVs) z&&bIuri^xHswnmONmEsOou!k(xzUnkzGp^KgVPYn>Fjb=zu}LL+6Q@dC(WS{7Ax)j zL6(Q@jhTnjp>cd&7PChVF* z^O8!>Ro~PvB`y$7GhBK{So@o%Kp76G`Pvv&g)L6ZlKFsAjpoGpXsQA(1{@hmSplYq z1J`$EMQ>THH(i6rw6h4pDR}L*_Sa72EW_E_JgM=?M;#|top)8Tw{i$57$T00)ro(l z7%z&@)r)vR!^c(|Luy3U!*Ox-v)O=av%p2VK`WtYumUSV3V~{Onw#fI$y=heSJe%G z4m;RQ$bwr&JIo4AbC%m_sC-)hL;TIJ()(EitrEzAI@YF$?^KZ6Ux+kHaMWG03Iqb2AW2m z#0Z6^F!2xCFu8P0kAKCqAi;xX6a93VF(xwrgtifuQ#SjlT{<3_11oqvtQi}K6vdRV zU*AkJ)`F?}PIzrKXd8E8xVcI$P9 z8eq|HvJP)K6I#Dy*t#kR6sPkn$-FJnx`(E%$`No@3tr9i(2l2Rq_vWn=%ZNMCct{m z65Vm8NT{;1@fFHk7(Z2cU1Ch2rbmHkp#iQd*fVFqL73zOc5y zp>nnk!IC&Mj}glFj_{ev#&D#jN72XPH1g-PkUxDH&X2z04qv_euN8!3lvJf~A=Y6w z$yy$i%z&{~Lu<*Rg5h3;=$qqee;lcT*vAM?@s*LNYn*ggdkwOsbCSo&zzk?e0r!-N z{)(p~Xy%@LtsQ%#i8`OvHno?cq;gx)6<0?E;{0R?whU=`GkIxABO`8W_7ruCw7?qA zS?9i3cpKDYme3l&VS3L2!;VZgw}1Zx#TTB<4-fK}@>~n+^|1>Whs@S-r3Y`FP8t%m!?@c~6J}vDe1HaI zezDO{c3HL@>M=EpLIf<-DA~k0e^FI4Fsm;IMlXHDq{^}>6{`g04-~9ahfBS#_-GBHGfA#zS zU2F7rZ{PHNR8C3J{H2dxzkc=SfAS~){QvsBpZw@YpS=3=QM<_EGfts40kzc!clG5- z5d8?i2|nh4NMww`8)%xw9RpL^Qu9OJw~DDlPipXt-s8NQnLb~p4}=>pWa#)&FQhC6 zJI;9tHR(z0=J6*bp_A0ozBi6^kOil#M0=*oPC9cQqCpbFBJmu=oiy%xlEi+L1~5=^ zF(kvc6L(O!K{za+Wqq^-5+bgs*WvOi8&g=>ft_}aLCK7j`M{?_y{S%|)EG%b{ZN3T zXYb3%M7=$e>qw+WE)8~#nR=+Q!&mLIrWdf47>7mwKNOB>DWzEG= zr3C{C)rEVQ5`>1?O)A-4%w#e3eDB-|FiA8`MvU!;d_^7vKTA{>FouO(QZcE{gDKA| zOH2e&(2yTUV+<)GwM5dYau-3HA=Esd31|AwgRG=988f52XKU-uImk?*WXr$^nAP1z z_K43{OD2~VoW;Mzqgb=L*mk-Zn@Ad9XDRRbaeyU;qSVC^3fh$Svw#2*@tIgy04)%~ zt(6%)%q+e3!T(n7L$z5IX>cI-I73Efq_CYDm*x? znvH<93QX=7iZ8d}vtAgpIa~=i{{qTr-X)bHjB!!(%2Q8xdDL;s-MnwoQ>LLpGihJQv)SP9uTQ7u?>?ZRCoVJTV-GgAiR{;KAM$GrDZJ^|d;W=S;!5B%4!p&)> zz0Zrcl&apE7(Jy}>9_u3>-mPA(AOR_s*PPE(hq8Loz)9KN^0+oEVYTfzC=4rcE5!` z4x95YAI8%>a2c2>78c^iyah0{%2A16)kuqk;BXiEDwc0Z1w)JqoP{~<4t0drqzzPc z$MXon(MYh?OWg=y8%+%$KNEa}73<=;_~QqTP#50S6U)OZq*Y=*Az;Q`H57_GL-QpJ z@SPu5Ua$^lcmF|h?8ZQoR?i(OBs&%0F6$ zQQq@ttyfB5TwY}45|FY;a|`IDEmM!18~x*TFBl~;xb%&TPLlJ!k9}pq;!GT|hYmCd zA`cauTD$;&_mFLB^%@_!@x=?U(HlY%cN|xIU zNS3+8%n);MFxtk0Da=p5lNi|cGm5d?N_TzCEY3-r<`z0Grb-&kMKUFT3mj-x2B3BS z^7vSy(&~FFFyI?lTUy6gMaaR zzyE_i`}_K}3-^4_`-Vo2Hpxi~gi_6UkDzVR9Hs*dmoSrzzP?qb-DUhzu=735ewHmp z*efyB&ve6R-CpNaB-6#4raF;3~I0D9se? zbV$k^S3n0ceySbwZM`+2^SL+5>58&;n$E>Nuew*1wPGPnx8&IEE~0@-PbEvdB5=aX&n3?Vbtp`&||VTmYim*}5I^Y^}}sJ`Oy*jLIgK%=P?^ zP%};vkl$G;Ti2unCcP<2($nhoi{6M-G-(%s@akEO$j8+Kyw0&taf5b*WZyHYxhkYZq&2G<4@p7!_4b4V+H=TxO(eAXH zX~kFz%l@`E=NedvJOD|?fnid+GN`jcqLHI152@p#Xez8RwVo37pHu3NbWUd@GTLRg zH?_)#cvP6FqlT52)zz&cLx*k^167vn!IU-Jv(>hLLOMjlze))}2TMWDlJ6}jP2L%6 zyG84e#yddDEyG?Cuv58x^1a_d(iem$_Mte*?!DxoP@l#ssnKpK4P~ALy!dd|>$WRC z4U1!zH{?W6bt39v&eXGA{JYfdxbO)oB`^Hi*&WK-D6)(B?)b|U(pL=1N7<@A$e5VD zD{Ml%t;%O7J>F%cq8;Y6{71x8jYx?|Xsg+x+?OQaUx?W9@0Q9qEn(b z;rXhvW1)quzQqX7UN*khgF+Iu;wN5?9P^$fc!!pb3yXHRm6F{^K9_KA(5r`ITS!+Sh(rX5aPyJX(TzC7`eK{Mt8v`MbaV z>tFubH{QH|+qWeaTcB?4c>R^HeD>2%|K&gW{r~Vsf1Kf0y`x&*?qBM3|8GT<&*F+c zlvyyf%q?0W9DEvwilDmj^nu%Yhx93qQDvVE88+ZUBY$g2Px{)rj@3@~%(XM?BKJtH zM`W~XEAz6H96$Np6)urtuBE;ta#%N@lQr6I#-z_usCqK5%FfQQ$yAYBZ5q^cpLvPb z9UO=~e?XOVj@e9R=_}Zw%#gh3N~nIl9NYxL^Kc_)*2;oq zMB-cI;r+h#JVY!QJ;*A{}#B5)~tn0w7u<S%4+dQ?e$(jEb_z zAI-~I*0$`-P;v`Mv6C|-0x{CqWvljVTnII>y0FP>KaXMs;}9?68k#sN2ozre*jmwi zsC@`llHT%?cY^`j0kUGlxDKYAk#IXe_}WVSJA@T5@l zQn#mpk!9D3xxcZF_o z!XazsE#nE06PxIRN3~?FAHN4RoQiY$++3x)5R;0*O?cy~5O9^u)Gu7Ic4$V?#q`Cg zJyGbr2bbTeiLC-zl8SCB4|bY2KE?pIDHVbHX`R{X2iF*>iui2~q|z>uXRV=##Nd#? z_6(3_zjq4febz*k{i;|%L(4)f+4XvPx{cQHnGJOtP&eb7hc!IsdfpbaHi%$55~P-G zANF!)PEz=Fa%fgn+f5c(*v1))BPh;Jd(I3XvhM->F_tc6*J)MRF|neT#{X1yPwOTpF^o(}o)ddLdd~;!1!VKqxx#)o_Eh zgXP1joh@}P;<5%{?hVa{ChCa6`l6_xts>34H=hTHIeNB3a?a=9n8-;vpB2CmTfQ

z$8Pc}3>q0=Xklmc9ndOO@Khb*G`YBKo6vSvmvUv6t`DbIj8lW$PX+VhW#6~<>0kWd z?PovzrEh=xYhU}C;(GUPuhjJ)Qs3X1;qAMyR+=xn;Vqm#L-5J#H$VO9AOHS8{Pp4}}u6Xvm|{nunUD;{saA1b)N~aPylaBumXty6_gtE@x`pILAhnQqRa0 z(GlabY2@A6K?CA3129Wot022p#qRXok7XcM`rE+4atCeQN(_lI_S$M+N1e&k@4#VMG zqvll0DuUY1)tGFrKG?Pg%c?=_gge-SosQaXZ|y2QR*_fSRnQVx9d5KTbEnnWpVsin ztfVvTqp9Y!P2j-KIv+EM(h?L2X;3HzGa|ek!?WSR4tTO8MxF6n9@E0|ooJxb4%nuX*HxmYn|8oxyqLZ3JRw{wpP9UT zatLjN^n@B1vXt!6H0qxAdb*`dpfHn)MQ2))`6kIFz#~ynJFH8Esisp~b|@uo31L0v zaKh8ZYLCr>qlSaINbZUL@x06@Sou3^E&{fY6AgJyB6eDbWdA~wVhK)(W*uqE%t*94 z5wUsEbeT(PrNSH70P|N%24KoXk!0jjvXe@b&OS3qWg?R(6UalShS6SI0?ml{NdwTV zdIQ@g)%Gk(=pp$w8GYgZ0Q`2P)rrZ+21A$i(6Ezq!BYX2+XF1g9XxC9Y-{2fm{Q663H0-(l;JIHh+?&ger&7MNt}<)}8Hc7}ZZhN3 z^h`n9Lo|z?hu#)q;fW9;C@u;;bfDSr*V%9Fgds3irnbSrC)>>&(9c8igbv~PZqk+RSw56=K>93;0b1*L;$)u zR$(AjEsa!+%`NJqoX)|6;B5KA{8&A!dU*_%NU-`GWP=Ez1NH!0u zT!jZe939N^0WrR|pvGsIwoy#e9q5KYc)%0H_*ssbO6iH+O18dg(5&lZhm{B?+w+Uy z_mC%r%Gb=TS|o*Ztq>(MepEv(CXAiq96UIq-NNYX#H79bM84>`N^@nk@ITODTq7yd z!AJyQB01w@Dm7ji@@njh-t+V3uYT}@-tPa^uYc|9-~3j6yZQ|3OF4e$i&vaFm}m2dJ!YiA_?_C%jSp~ud!#gL)6k4 zLjY%E;xv_+xN%lr_o}oku8}f^reCl2k#jR%4YSVXvXz~O)ST?GXIG=CsY1EL^;4<9 z80F5sBFaIP&rQE%dWfNL19_p|I1e$6%;_odqpm2bo}J&d*c!PV=78h}^sBbY zS>YsR5aNsB)u;GUt>LgA#0!SeJyb;Q`o~Fm9W<77&yq1ZNjYma@SZpZv$)!+BzS?? zj6if&7BxE<*sZEMZc#dW;o6XEe07dr@aYY&R54YWODuy!#B6aHLouWx7S}QFkU4V2(T?c!OR4>CU(pe{;cN6B!mE@;(kSv7bc|j(ENt-R; zlQ1Q)j-P4nEe>7K?bIqfFcjatg<*WvDE5o`{D|1d866)6C;|ryMQ6vM)xVk$ja9PS z22|UOx-qojJ%gF>iMImFIv*q3`nB)|eGO88`~J`1D${VlN#om8YHzNqbdlXGuIzIgte)q8du7fQX<)hy zYqC)Z9<|N>>MNj_a(&On=6%l##7>SjPxGu^kCO1#5Q4`okXat*nH)NN)=Wwq2pn}< z>ytrXmXz%FjDtE2C9VfMyB!$!k}13URq1t@Xqf$ATM*_Fg&1{W9`GVJvs6iQI;>g9 zB%ZTi!``AQc=L)L&L4+bmva!i0Qen;5L?m%LA_C>Wl_)+;W~}MOh#K?h#zDx#;7w6=sRv)l4&=@(d2U~Z9u!j>Iv$%mj3{vx z_MPG=6gwahoQP=HQ~|9PEHb%*Bu7xZ}!#N4)%fh;bzjSYfA>bIXTm;;aZ+7d4b zE4s|>HK+_u1H;o1D`1kF_jW~|oe!paE`fJhN>q#qi*2E$H@^-i#y}Fg2?P2{o;9%{ zKsff)%}ykna#J(MZ6OwQa8w_jgBmbNXrFpxuONgD^)oWvk1}$TiPDLU8cH;s{18qk zN@}TH2m8V6X*=!jQDZpk4{ERkJ`4jyBW!_9qGlqrdj<*2GJ5AbM@|vRPOSWx#5!+U zM_c_wgXy-mM44bAnOR(sB$u-7)=pBgqcjI|XF2BKsSA|s4ZW+yk|vJ}g;iASj;?@Zjn7)uZ%+ZwSVB}X z;y{03d(wlBo(o`o;fx8k+HprL^e$Y#*BDU7;qPft(haKj-CV^nLLnYef(uJ zW0}0VVpzPkhP*U<@#wHu&|dZa#UKChFMjmHAH4qL<+r}q_X2&n8!TBdO<;M(`^wW- zUw--GqaXg+fBDzF^XCu$)tjIG?A1#jrjTkb!L)I%jRpB`)CnIZKqJHny5|Xx7pYC# zSDx{cN72p{AzDSPJw?~&ciM@cET^h{Z|i80xf4x8pmVaxs;pLgc~9+Ny? z4&1mr$&H)5JahU3J42F#UwGOB7`kN%#ax9{yJP~r{7lT=E(T0%5}-31s%{TKhD9B2 z5xg@Wo@a=^hBpbD9@-VSXBZ%87++Tnv(4H)Xb37ZaFiHIwIH+wRAdvS4mp4U(IRGuXmql22S@#8-47c<;FmYg z{E$g4*(OLQ)oB4Jh7)20)Y)muxlf>MLe&HE2odrSHGRtpR|mP)035o*lwcnUsg@Nt zM0={Q>n#_BUlt$%5Yml2ss9*pGDjb z3e`!OYC0P0P8b_Km6&cwK$`O@?wFOV3THK%T7DSZ3GoA;&X}4r(RlU6=#mLtXm@+9 zZlr!Vb1PWAQwqDh!+e(|nTbqpjD|rhUH|}nUE1RhZLZPlSGt7#8x=2qQ zOjsP~_P4Ey6=;9vXu;I?;*;r91FNHsLlvW96#-@&D=znXu7Rq!i?HMFb6i5*c-j(rhpZ>b2HzA+ zl5N9x02htj@x!xO#Vv!DW*bX~@CfvI;Gt(H9hWaYd3BNefx_qq`@lZT*=#w zQZa`x5q36>u9j=pdYBsWnB2Nh?8N%Am|$+|VNeud&vIs58)VcPGnqww0uQrX3N#Se z-KdE7o=LAtQ8m7J&^o+BdiD-7CmVulXEbGJu8)0C3sn@{-Qc6zkftJXXVb*s?C^PT z!Y`zIFDGmi@`I}9%Fv-$Xp3SD*ki*VK+`0Vhtw(LKQz*H~?p~ zPIr_JVxp@e@(newlQpAf1QbcAHoO~Y5NI++HM_%e5Kt8&6~%X3U$n07q3FXMq<1_` zM5WRrd$S0q2&v4q!|^Ki_BVL=fseW8qeIL=&~qXrRY?|S^G>!RkgGWw!o3z*bEf-j^!Ohv zwpi>h_qEr+QWYx;WX!pZpneS5j!9}Jd5dkVY;;KR^}^+^tx=Jdft{g5q=Db7l!uIE zU}cdl-tsqVtBLC495Ohwd6zQ`c=P!16V)5*gV+;U+rY1VSvec~n5Bq97^p%TDp5~r zSu|P#e=N04B)@eD^h;m*^soQwFaG=oFJ8R*3eWjM9#=bdCCfC@ z89>~N?Vb#yQlv`EY$Ns9n~g+;T7EFnfsX(HKmbWZK~(E@n%^0*@J2qn3i^gOjk!uY$D2zTiGM z$gERQ9b0YCNMbL(VpbkL?w@l^Ubx2o0OoeCvEAglw~nUJkxxj;S%PW+>iHAtNcu2l zbh4QNK`fIQLS~ICIW#3P&Md$Ia(=!DT77149Co>yLO8q5(Epqml4nlE32fn%5oW#h zQCvL`&S6_rE{o5INrQ}bHu_l%Hx$In2IvtfgKnA3M8ST@u7=G>wQ!fp9ESM$1cOym zw;rLt?4x#~=aanPDd*IgO*E`@xXEe3!Lf82WOda~j7ony+*VgwVzORio(Rkosky6? z<%>Jt#P4|H;%1p5qX_enD)IJ%%S1cp*B(jfSZM1o32~{f2VH_y4HSy!8Qu@+IBNW! z3$Cc1@=Uqyieu{61%HO)au6q6%pDXg45~DUu6saWj(%o8`a8dU#PGWrPW^Cg5fo6i;6UK6X_|X?Q@VKufcjms zEYpzl8v-{3&DU_%CX)+t$F;>;{mGK4u^&8HrnR5dHsJOs2v3TX{aJE(65dJ4OcGp4 z*wI8S9UfT4)IpF@2jf{;Z^nzpx#Wl0)}1|xTyuX87>L-W935+$Pt7N=L|8Q`)kI`0 z7((x70Ct?RE7h861B{*XrDYzEBZu-fekaP5)J)qhE7^$1W8}c_X;k*BJ|Ni9N(;i8 zjU4b64zq{4JtTOTz_-y`hrS06Zgfarw%o0CRNL&g?dMn(T>HJ{zH})^m-hh3BOk+(YI2+a*JB>U z%6Zm(s2)b}w2b+U1`0x-3OM)_6@$a6Q3d2OOILE&6(s+v2L80P<>dbB!ctO`tPhjL z2P@D!3Ay07_-8%fCK)rem*KNBV1P9G&oVT1DHP+VtVTLX_}9ji@MSE6Zs0WtK_zm|@>^(6Pyfws2M$ zXS;0=`O6@Uivq%qPI@I3JpF=@b2O76PkHbMxWfgY{ko!i!I}Mh?4zl7$(4i2%u*Aa z!ycLL+XEhc!>W)se6ts#aaX@xyf0Kv|2ib5vfd;sGM{ zjlN8Uu>{v47eIF3s;~5a`SRzV{^T$J?9bl6d;iJTzw+g;el3OGWbf_J@8AB#fBi52 z?w|g_AODko^27h{Kh^m0$FD8Z*$f&e;Va-=0m^WBU16|avkL9mStemEd)5J)99Ftl zK5DQA#+KcIM2b3dCY%?FBed>*ZQfSLFbminh##jgQD4xSnI4LdA6Y` z$h1c344{aa4=9%NjW0lYWK2IU*RnvgKSQj}?AKbqjN}qjNrr9d8JPNZ5^$Ct&paE- z04r9(;se6Pk}=OpKi3S~S(0OJ)j|!D2K{8uhy-^vvFLNKD5?V|nJqwJEm19y6@XvD z+wL7DWnW{qx5r5nVgD$piN(?YeION2d*$)MZ;jF%Q$?Z~s>gx2ldfH3>vtmT7=9@W z9^^$pl6`?{67loKms)r2JaQCjW{;~8#|m4JpT|Y?)B=w>6G1xvXnzRVfjep@_#-?- zlYz{NiL(^b?wmC{lzPQkE@}^4uGgPmhclWrF=lh$hZ4XJEEWfq#>}*{*sW*;TVe_fp_<05+<(SC0UjiFVFg*d|TcLi&IwpRhk z6S2n#Li>tAWo;w%|(vb36(#&uXaHBwV6YET1^hx3>gyntpdHWUB;@RnTLk1K*CU=Ccl+)huEk+k*)F%&CAU;oa?mB367%@!j? zl*Xm18h}LdPu(vatk&l~Cj=Rh!oE7kk%nvOPyrx>0|ahU6L2QMsXwQE`GV@*-2=tH4( zGe^jcj5P*k=boW{2JG&&1gkd0cnyaW4}6T^><`)FZf9Yol2dM&FDj_$mg~KXiaD<~ z1W&ulNuWXtOrA;CzCziN5pUj*@}s0TcuV!*r4k0GkmB8WwNhA!A8ZhO++u8j&Xl-N zAOm?=3Pc39r?i|gPM4a!+FQrBMdxL>=gA21-p44oSxsiK2!X4eNXeIzPDr&T5rQ5a zi7>!m zqksDcfAFXO=8r!6^vB)!PYi;5g=8K_FQ{t6d4f%o$U+drLSAl$kvfD=3$nuy2L(C~ z*=l$}jX{q+wQ(1cxeCrtN5Yu%iCGZs5vmXQhwKbo*Q$V=+E~iuXf@*cG-2fE45`sN z;wKJIhf;Q49$oQS6&hyVz)jY z(z4B~$lC5+C(R-UN#$qt-qkp>$pZV79P@Q^Qypt--i{KXIT&wvq=5?hh*TjTG78Au)(?th& zRU}cpA9P@|0NmGq>;LTSHnS`yWxb!qciOpBv)jpW8UqnK=JH>B-m=oSS0tpOK|yfw z$TW;Ws#EY<9Qz-B0s^i?)smwdWoazjVrsUKngzlVBLJ8i##a*$>bUDTOM99N{xeHq zn?L>cOTnYvrLGGaR9$+RIYz11fTy7D)eQEL89J zK_LOww;^v3wCF1!`4-lKGnsZ-_Dres$M39f!+SUiLGzx*bgc?+PZ-n*SWgRKEAz#N zN``eFTQ?gDbV*3DkD->heAM7FD1|I#s63ivhhp-QQUo$IM=4~ww5UME#!-GhyePy* zycKmp9&bowd-}Rjio@l5cK{}t?^IyYo4R~)9kvaEI&djAfe6kb`;-@PI`Gurus(uyv3F->|Yd zD45_Znmz_xd#CuWB%^Tc=OSN*Vq!DDn3T_U!0hp$<`_dF5sZ|G*ZY-l{eU<;*c@AO zrZfBUdGpw{Os062wftOpC7h3NYM#x*hnxk^7@D$Az*%gia2_wi0vM^ejc5KY=qFT% zDLo_2(8<&B8AJq$4x_ba;Bs^li-m!dltWj3;uo%MN(0tgqdR-J7UO!2H%*XX`^XS( zd;J&?6U}ati$A};MKR*UF+h=od(h>>)s5k10h^N%rqb)Wk1$xu!8FD)zi+MKl(Vdg zfq|i24Im-|ZVhR!8^VEu3^T!+jzGDt0^~yFjn&z$_1(MX?T=o*e);NELA-zS=Iz_J z@85m?(u)Ky`}Zqf>N`O@;bn94wM2o%{8(CC1-?9Fns+g-S6FfO4&F4Mnk;uP$4(P1 zQa#`=t960h&KP3L%%4CU6GH=+nX)R_YBaFm~I^rT54}=@hb7ld(R& zX5ndlP6HE8UIj~3az{F>Fw6>%I|)g?IWu1tTO<-I@xmaAg@aiSTgn0#ri7IJN9=(& zY7(uJmpZ+fWhhUe&h%a7CC?47p{+TRwt%u)9DtR>7qAB?BS~MeZE#UferIzl95h{C z2byao4SRlKdv{Beu!NnbI+nbK0wLVxwV#WqCT~03h+Q|tsVM46s8p$;QkN6XP9f@f z!57`8|FgPix!O5AFNR@HcY3&-hS-9B7#p`O%DT;ifC?X z+A3sZW_Idn=P&i6&r^5S8ejR4VD=Ubhjp>5s9DW*4GY8wTlfmj%m`}L#91rNOf}w3I?5%%h1w4HP&`3uUboCDtmLDyY@y&-yoKN1`k($VMpxO)QI~eR z8Zt7B9-(XBs)1t&w9Kpa4JV|qzDfj8nTz>?u04Q{MEeE^{u7jwAs(~IqdGX!#+(@? z+r}OYC^ZU`01bszZbjgGzy$0OaBs`{+_k;){<9uSr*|9ddk|juvLG?u5X0b5nQbg$ zmZ}LvC{VWJu^vi>lSaHZO*-c_vDCcRe}tBfZ>s6P-W(H~*t3a@g^qm=rFc1#uAcnZ;OVtHsJ(;K!s==;k z+01#};_7I(;g?0AE(yyNatH;7ksBuk8141Uk?gf&5DaBDp~KX#eIR+-B`Y91S`vBb7bEJ5 zlX=W)y5JVfBAC#GxuC{;&*q+u4O}@B&06gp&h^9`P|?{m!Nd;3XRp6x<&jh)t(FgX zD(%&yHmv1zxIA+*k4%=#DJ!L-tM(-@2`TF->3U2GV9>1W+Qq0CqPd>kLEcpIKm#c| zVF1H~#gC}7XtjV{;4!)xnss=fP1V_?$I`!`Yon1$)$Gigi=2_t&nncnN1rm@Cc$r- zgrFxb#GZi2?woJSO7Su)jX2HOY>USfUvUX%d@xk+)l4Qs87oa&eXOYeiQXH3-o1PK zHV-fRr0}cPAHTkPttrFWHl$BN2mXN*HmwOmnS^%sQOp@mH9F8A{;tx zkdjSw2GUBm-2!wfGY?eoa+L5HPJW{eL21_5GOS0&ayFCM>tABryAuUGY3sIBvUtV&+iHDPL{+Da) z3{O_$m~}B2RF`CNi}cXyO@pNkgPPl$h15(efaJ5bWL!eEcRnJsrcJFMe53Ea7&|K> zMkiV}MbaDa0RUGjtSxwC&AK^wre0I`c@>2rgRi852a_0fNjDX~_9 zoTH@FZg~(Z`aQ895l$oZ2Nbj0fyFyJOyv?3M&l>ruuxQCW8X|tg^kgLI+)o%bI#vH z%^7AEq0jpWk=@E8aJ6Fmly=a*b3=00!kh3<&io|Ir0)*Wcu{9Z`>e3VxD|L!K_+9>9saqfB@l8wxEGE^x2>X~+uszuHhW?MomNE&N)g}+gz z6I*(aK(FhZV9k4(GfykM^m;G!v}7leTssN0&91oS;5oaid%m;JY|e$ffdhXQPB)#d z0J6QLZ7t{hRsiNx2>C)-B7Aii^_mRZl&P!{?{aCEB>FbFsxM$z!cbOJ-K^%~ z`f^c=?e4c36^RxJ1RnHn9E^%7lCK0N)^2{BHL@e2S7mk9?Pdh7B!alno(nFgj7D_B z(u3e0?bn}D9qAm{kYp>3;2_8)b_S|b!C9cI^mg}toAtSl#3o~+;Dm#1Lz5u^Uh4*U zSS-hB&n=IxL(@1-b4~}UbNL++jTpUBlNvY@m5}MEjztlWt{pAHy0drKA*xciia2kK z8j&4l>P%+Ug?0{BWh!Uzu-cH>w7ufCtjSpO=b}0T(v`fDV$ZxrBRTYfW+HT_LYWIw z42%qdCKr-MNYL82wC#x8^M`%b4UFS~>fB?v_Dyj}Noqd>kP=t@jVQij!L9p62+osQ z_#rdyYUFT#7l9C;bvIqCEiPBQZn2C;&~w9%+l+RyA4pJwS(O}B<7CLr*j6;4WNMzn zAg2*@jAdajC|G`9x$A&H;(H#+GV z2Sd}YXM?yoKdVgE&|5~$p^({n)c$aIo&kjs%RwK+VH5{ubhTMP3t{c{COuYXR7i*B zda`Pc3Qn^TpWI}|9APqBRv%c?MYzdDDuj@+ERJ_RQ)4*wnxJd`J#?dvV*R;#tYm=| z195;7@CYy#*B6{hK#~Y5<=PKI4hBTkQrqAl*zsbaawI%t-xTTOHdN%Ex3n~ZhhM3Q zNF~n*zYQdsbJ6og$$Fd5uZ)Z|;ghz`zhsNUYisqST*b2Dm8R!x>HK9H5)tKv%uAGK zx4S#dZRlS#i;_H+Lz78nrJzHDee3}7n1p;~c<7skQPb~WrYyxMtU->`$+!x*zmxq?VG&s>t)RK-)IEQ`?MNPZ?|PBWZmvH75 zZxYEQ-1kE(ve3+y^>t9KLjr>FfYg;$<&n*Lm01OJNULEpZ=gulE`&nvFZ1bn!I_m> za*-Kv%`z^y?l+gg$U|%{-i&*PUXy~dAeAX*A)mXYrOk`i-}@bWbDJPZ?-A1U`f zgYsE_e*q{zna^n7{j~K;g|8nCVJAZp!ma;0vpA9#c0UN^#zg@Cx$Je&^0aS6!Edb% z^$&pA>L`1XU2xe5R#xjF#Y|cX2npimXNdSCTob9<_j&G-kEt>#)NO@f07KruAGa|HJa&#W7ju|n8Ou4Kth`PIsx%EJB1;4aQ zAkq@j2E`cwIaMXH-uh?B6vEfItKaT46nsTKpN!P5e$vBC&YW4=O5qO13w;r7bg(Af z8f!r+p}u#$ohfdo;`#jf7tiftfQg;2cm=dgdxNnvPwF$Kan_%_WOZ*ZQ3q+%x7jkC zY!qm+U$C=C){@=u=D9i@i*;=tsb*8^x^!cm#Zu4T-A%7ywTHG0>p(kPR`uAu|ZdQQGm4-U?*V+pIBvc(62EsL9pSX5|5Bk7)FQm^s-pHXyN- zb73<+zr+NTYtexi&|`3b2M1~vP+fP2&Lpf*uc1!E2GD-u*|vuS*=p7>8kO2L?JbQSaG z`!YpM)3LQ&E4eq>(}ux_Q%wvJVs-~Td9JBVzrx5(S7V(Q-2SPf_Cb}0|)l#Q|co?b$QD5m2F&|kq zM3)O~2qW8ht1HitpfH0qj=TSM?U&RAaD!{#Rfcdn!0T@-W+Cj+bGG(58GVH7ZrRPp z;#!oL#YImvHufT>L$!F8BeRIJZ;m0ybRy0#ER*Z70GHY9>$H*Qp6pI71S=iWMxC{_ zX_1y$sx+a~KDLJFqRU3|wa(kde4<-#%t>^LEI8Uc7NkmO7s`v8!!hASfjWTO^Fm2@ zm^5cDcL>_>WN=0XS8KxmKUMdd7|D@bS(qe>;{4Ujv{s>R?SE}57Uz7|Jd2tH5D#~I zT)SrO;SretIF(}$kn_`zc2O|5r9mPelZ1=(F12~h(n>6OSL-IW)ca|>a80kBpk^&P z6y+8|N$e(hL`!T!c{KXtVXh|(W8YNUUJjY-70S;M3SJ7T27>CdevKlkQ4y@Gg15k9nYSoL|2KQCwhZcw$)?DSQhEL2K)v z1jLP0_rJ%1yRp8R2fEM>_~xxB3{8n%iEUhmCsbJQo}Oe z3Yd~ASWbu~jLe>c-bwLkPLP%V7)&CqDt^(kHQORBh<2VAR-ZQ{0u|xxm&$=^Gsd}} z-$l-G9)a6SQfTvSKy^li)=|~j%5y)$e{W!2oEx~{t7XbHiI%8}{cP+Ai z#GEOm_?tJd*;=(=K;pt}P@I06+UCe1@wrepoj_F8h|H-m^I4_fylpXv*x-7W|Vl zd+a$U<^ZunV02ODR06V^k zYo~uOJkd#;SO>;VU)y^EsebaRkkY!OPe8sfwI(fVAd`c0(NXIeQ)H!XI!=h0TtxUj z(|SQ_y9{!17)*-;5|8w$rQAsYQaBFkEmg)j2t{p7C6Y8##*JuAODGWh1@amhlyP#h zbki}Uz979R?Pr*^$WoX3kJ{{A?!>j*P03e4kF!PcEUvBu!pW*t)$RJci}F;0>J4)O z1pRVXpUiaPDOdA#7Iz&P3s7d`xyzUsk39onvNRLh>=vKMVEs{f-{$1f42>lK~ z{|HR1DVx6Xnh@=NMsI6NiCyyi@>~{-MWKCiH2)cmUUf7;mz<%@=~KY1f((bD3hrF0r-F zjS(vP(3^-!8M-y7d;X8MC1!g0BE$CZK|KToxM#fXLPB!xV^GCK(h+{ncQQr^;!KuB_;3vpx-?NU= zjs!<(ca+K)BEX&kISOA8o2 z{)>QB^5#Gd=NvSXCJt(mD9&pX#WWw(VW=T{9Rf6sP8W=*_S!Xz{yh%~ezq_7j=2S0 zlX43ivt1sMxR0 zMb8^N^gcw+;l^g(>$vS%eK={B>(L4Iq7ohNy&l9g-G(NuMNyB?-H(beD=3{_3@LEh zY;ZaX+kf7BnrSBQf5%S3rs{;CSUwtMdNWnMnbY@RWma3<@pLh8xOzzGq`ROsIH-s) zs}Iuf;_ql#)rl@ClUdcf5X3b%3}2lsq|udKOCt4B*8$GHY4>#URwPFU9s)b2q$Nlh z41BJ3r&#ydSXa1B4rX2LZE;@9=uFnSxKqR9X6v163eQ&QZ0G52c8ul*Q*$XZ>V5yA zD(^w%VS`NAXM1&)R6V3kA+p$8pBpiFgiqXQN`sOxS}P@><&ACX6PV?Aldt;503Alh z&5hQiAL4q#!;fFHCfX%Fz0Nu6Xr+)LO%YR*iL`x7Xe4}>3IqFPb~|b*)_MSU(dQ=< zmAJ&S3w#IaA;;=u?Fv{9dRfLRiR#N}@kAsYAZT{j(k;ZgI+-)Q^4KgKv|Ujd2#HoN z$l4`sbk~L{zn97ezwHUkPlqv-zl`*aYd!5YYie?uKyv-%gW(v;+AcM`?G_Q{`Z+#3 z2RUTP=7nq5RPsQW;N7Y46aam3z4+%dAZ019m=4fjXVc|jHE$uv!!6Bw< z?7GD~Hg;cd97~aLiY>j9VB$o1RyUinf}BL1#Qm`y0nnG#j@_8f+u?%VM}YoIwZ+_Y z<&7*m(aMt}O+G*g95mT!?Nm7z*)?$sy-DcJY^f`uXU98y651>w_B<}|iPT)jwl6)b zwEPoNZy#?2!oas^vVotCe+>aMw;M=cV6yIPimPphorcvkDMLaMKcW5AFpqQZTB9{P zCTMiYmZ-$4Hu4SC1t}s_LDg;v6c^q&K@S`Ln9fA$S|yYV^k#Q-3>y>ENFfk_*OJ=z z>(&^1g~_8UM14Gw3c{{AOI_W}3e~)4DJ77Evlv;A!16Q*H{Adwu>4Lr#T?43qx(tV z!NQWtR5}OZln%F#aK9w8VUBO_(6T6ZtzOfVPKh+u+#lFLI%!5EjYIFgNJ zxX0%PUvgADi<9%5sE1EY3Z9LgpcXY}C$VDjOJAlQJh9u6Nzp6LM{l(F(*#zF1HV5k znF)4YrgNdshV#7KG~gs~CUrSg1V8$q-M9XsO}98-{mrOS@au1VxIPj^^0-V+4wUk*APIQv}q>cPTPhC#z9oK@I1k;148KTBaJ=7d^b`>yr6M&*3cz5z~Jnm6Lpz?c?F z`#c2#F58Q6#V5`QW$Yk|H5v^Dl&u#xg}E^X`uwwAfd#lIGi?&-AbUByI?J+l4kpvq zTh-k%X`6XhFxLV7*sQC^}H7}2gTcwJ=TJi?)qPT{U zA)$RKpw%zQVTG!X3yCMh4~-B1MZSZ7E)qBJ>VWq;tK zW`nG2-v$;0dEjMIiE&LMVUrcd1%&&kCh_#Ljf>}aYD|B?^(JvA$?}TeQ-&~0=Ap5f>@2^eq=Ni>4Uk{;Ueit47IVgdv zEZ&GjQA)dfJ6`kXFy?8vc=8>$fq=)pFZ3xiPGU53&e{aBf#@JHap4AO8#GXfdlkU` ziYJUUc+(-SbSV*m7~B{9*^2q$YM`XKbbi8AO4-h-NO(m9^8&;8j4zr+L4xA~Ad^?G z{O{d{;wmdl&TO{C!+Yku16#B7C0;nZMxnVbDD2TSpeeb|jKd*F zE%ZS{7*csspu2ZrdLb(Iu`jF~giM!12d0>Cr9V5xxZ*&MDEb1ilhxjmwOc==$*5Q$ z4)+#N#_R^b=p~~B@*?ML?uX6LTgTw3u1fTe{t*~~)N65XBhEuQIPu|pR{W-_;cMiF zLbg+uLI*1sW=a%7Q^eAT6oa_ttS2bsRYZ?HSIZay_qr!G8VWEKKTH`{B;0c1G~6^e zn#EPUzMZYWWJ1N}K$r!}#K#w}yPDRA;;UK{P(jwzA813NDp|3`947LbQ6pfNhO7)3 zq$W5s09;w%ciqL?YivP{z`G&q)*~w5XuA=+kQRh?iY-XIL$qJO^gtsFtHXrQxJi4l z!ygJC(c|6W2oQm0t$9jzD8v0%BvaO_bzu>jLhkSAT@@Dwr2H z%0cMM*y;@NSIYBJUlz`=;)wrF{1x+EoQyl54MJ2gKu;z7iKJ|n&DHKc9!W9B@K^(qAnaHC!^#&4w zsxgZr

DB?B!UW_&1!)k7&^gP2Z;ns2nX2p$`bC0alRq%!R{I|L5>N;h>|7V(1*X zuF6$g#!oF%9--20x=;(X!ybgeY>w&>0vZ`04dq=2Gx!ah95iRMu5J_Rt!}tos(qf7 z?ke1IAUT0Gjdnz96BrL^?b+2DFjR~t&pKKa0-E5QIg<)P%GE1dsx^Stq1VJc#@C4g zb@jvxxk$6JN+cy|`F5WYl0lTn)L^{}X-<9oE%7{iy)x?v0U1yyJD~$3@x1IRUN28A zLu;Q{rbqZlTj#>hm{c%1%1lojW^hLP5Zsjj=8fG=5ekY8rPg;}qePJDiu^`HE_}FT zspwco81K4)(9YE)mrhqE8z<=~w(&a;4ii~e5?g;1q;?_$Gb_Us=4>~btTZ?^1XH?T z#C%nUlo~h3)XH$eP-*1<6`deOfeV+w^dCke)Sj)jc`spGqhbktp@ z>+tKEW%_j#2l>l*#^xu;;RA_RBrR6x4s*S%RxPST_kge5MRDUnEEyR6gV!P{z++yCOYg3=Khvh4zwEs;T~dPC)BwA)ZJ-o8xLoxLsd(AsS|JJ^Zq)F zEF>DLxm0&pNMtMh)|_u)Qy4u4{$_SQ(1fz$I=Ohx!{W<8Vs!=*N{p!yW#Xz_4{ae; zo*llJGQ27SQH>>5%<#<~YXx)8vWvN0@tzWS52Uk&u$r$UKx1gQ53!k} zQ}~6gPI9<{+36zDt-AHHF)&5;)7etpl5$tDlFQ0o<@w~a5mFZMCH2Z#sZPON=quX;#qMJv7BsfPht0#`9uSXLG| z(^+jhLzxpf!^mq~3wv;B?dh{v7v_8%ff*bRvlHm5B`K3d-euT4ur}C%f+|a~&75_3 zmg-7_dx;0XPO@>>2*PSV7Lg=tGIG~>OS#RnVOvN2aGKfj+}SHZn!C!*ICm#LqD2jJ zZJQ!D*&58pQVuijzJU)hu`0n$02ZSLw<`Mex1P`o`b6iS(8&MEfKWu)_illzuu(Op z^3{Kituu=JB~K{jfZHM)@Q(OvY6D|$0%XFx7NjL9s{PJ-SCy1#7MLEtVo7shq&uB# zC9`N}7&pt6)tY!Q`gYYTH{1_1!sa!pI%Yfx{?y3*CDP_49cx`Uc-H@Dv|}zx6|S!? zB;M>eu&~v?Ah4l11fHN>*ce`<7}^{Njw}=!d-1i~zW;AR6Q%CRZPnnqP=%RGYCLy> zrpq?CxfNAsE9NVU6#_%`hYGienCk^YHi?C(DztG6n#&IxG`V`4kpNUJo6{+%zZpYr zT{0jBZb!22-+3%=Q zm}?`Eq}Xx}RwTIw6sd3DDb$tL_Jwgbrc!fxjXWQ28${7_}l|G9O#1L54s#H0;J9ud(L1S2 zpYL7T%-u_gNSa;NziWQB$XzAT71Gk2M(Ls#B(M$MCgvH^k^ZwofLx0GbX>~f;vzx* z`h=DJemd=|9n<~#l-9{id#UFOnwlLVum%HTOs$|)6V7WJ!DQw798C{Jy?8@dZUmSq z1v3|n^U*mx^=CVz65$)62I32m!AwN2BFtnUCfQ9uH%q_t_ZZi1ikM$P)RLYnezvMf zK5dMXaf0ylQff&y`$^Wt>1Y&23b$It*Wjdn`DSnus%UPL-YCu*1R`XrKGVH5PS+7{ z-DLuQ4q`IX&Jv3R9VFUjH5}(m;oh5BOE9ZOrVAGPMYVFPv6xF)mR3+q15wn%flB;C zm#afQ_z5^qwS7kr^$nt@iM`gnu5HGmB7vfAg*hxBZ-rDNg6VF;caj#3YKrZPayM~Q zTy$qOz zOpD6~=xJpI_R9&)tZrV-U@E7cZ4f|;m7C3PJ{caUApKxh#GJl@bTW}OBS*c(RpWp5 z!?78rp65lW>^QSlw)KqS257KX5M_1qqTmAM;c^M)mT!zSg&LPTx2KCTd=gvEL{7d! zQYMM$7!f>f!h1F@f#^#hiH=N%QdH!C9gA~hv?UfPyA0)Z5sXo5t!#3_Utm0I&7P76 z%_Xk`-;9g%gzXIYx>3?i}zyo#mhfz^Chb(X~6(jCAwDMeQ(br(*ki zrI3*`6OQYQLQ%ZVEls_KQa1p)T7Q^H;N4`ZV?pA_62Xg71%{)lfG2*Idcvps#qFk2WstTerza zhz8cp@`?)1c_ai z+j!+auUWhyMj83GEvm9vdvIY zz20!hBYp=-3|nuU6+SZh39Li;?PsWl(eN&Y<*yEIz*t_2Z*}M#fS{s!i(VA;RhvM= zD32+#Aa!XBa}5ZXsJ6XFG(TpyUhEhIC&WLvb(jroS8^LTze`#58jXde4NhMv> zJCuHw_POaRI&!kSwWWprSyR_86rP~=$D8Qo;Rw0B%rxzAYd6w1Z)IVSZHl>~F%=}p z(e;=_Ty+IDc1N9oNlb-?Su8suB?6%~rZT^rI()SrzTh~Yo8-uj+l?10apuCvhtcIq z3XrJ<7R1O!t+A+n0K2fqZD%rA2$0pZ*JlcyLIdC=0A5IK_!0pJF+Z-0W)3!5%;4E68eeoXm*f&eK6~}!}VE-jj+V zY&O^*mf&ON=kRjHpLnx1P8z2E`!ioRzb&U7ugNqOymDS0n0CheRs&f8@i@NVR!9Kp zfa2p^6O`4wcW?rg^+nNELduIhpT0(yzYmxcoHIFVVHgL>XZ@-0Bn7iGS&>ztO)H?7 zU)b+hT1PR2&G1{pnSGL|J2Ye_eE8*V7mBCG&7o>HYy!*ZP9=trmB|R?TZqSI$L#dOau1g_M}#Yb`53rS0>_QYX=+;y zsTkJlhHNuM08QC7d5@N}UAP3MahJliGisEipht898J2A64H*)Id_e2&81D8pmQ$bF z%z&GO(Yma4HI^-uHO&O1U^sV)({WFWl1Ki;pvz3j+q34fZ>oeTH==JMSm5VKla8%e6`X6n4*+C7IMto$D-I2{_VOv!WEei z>1Vspu#PtSWq}Q+ZMOGFb@4Z{1)L4o>UE}a20%_{dC@mcu2_H?2OLmv0@&K=s$YQa zN;s{GZhrEYO1Z@tU9N^osNl#*G1jTVyl}Hzz;{6PsP)baB{G&a$Jr>GBJ!tUvtR=3 zLDQWiGpcX;LgxTk;&90LXLbyvnAB-Q@8ACMPmhWg0!Q(lQJG3BU%t}lu|%7ttkb>| zN=Y{KSHT)tS^5Kp=_8#GaQe;4Z!t3mBxYt_67g0tzrIITB~x>;whv!@+^F_62jvhz zn-WyAQO}O8p>`3NG+sayd!V`np9MGLCR6rwq=14Ib$epf@HGzev>m^9c&ukVvc4bLIQox!YGYql)Z0CB5 zl3e!cV1X)(4rN}ZgJor$wQJ4m=Ar{buXg2bS=Po+n?s+oQC(wAXukdiec7I$wX-fD z@uEGgYKi3Lw@Zz&-YN_#JPNu#;Jzp8b z-2C<9n{ulo2ie(Yy1jX5QWgLixiGGBq@D2!Olky_OlHe(qFqAVUwax?uP`KzX0i*| zKJ|fYV;$6~0-ZlhvnFg>l9%E80Ib&9cweuIGNxQWO%`n!rlb5d!Vm;=F*JAH!A$WS zm>r{_A?WJACGR*bI4i~Dtwa8bB&8Wxq2B;ZX0B@I9!mo28>Lr4<1I}5CDP@y_s2EP z*kEEy-^~I#a<`!zPI=1r77C=1K~<~u%p7n0Tw5IpVY5ZmE){{BMs>ObOQ@IPn{i{X zN##|1Jvonn8M00MWdQFInkHO==o`xI*;6;q6ofSAL_WZWuWvOe`E48}DEiDrb~o2E zr%e_TLV+zLKKgIDe71ut@5jH7!g6pI%ySb~Me=q*kjTVq@!dK9ByD;E%BzY$p%Og}`a(K=*#eJ5X(L+3gX2@P`!fVpt zB{y*Wl=_5OU=}jbRZ)2~!|&2m@(d_83rlK-vOgWa3&Y` z3WI5AZc(I5(1OS`kJ~>e!G3p-r6#ADat$1u@(MGzB=5q~o~()!EkW+0p1t@ho~^GKP+_ z+FMdnHGSo16+pHY&}x#D20MHpZHb?XB8jEXe5f;xwCo>>0__N*xdZGTsC3g5u56xJ zLpfj8^9~kF`{Y-oIn5Jrxc62l=J}{NOl~{mWWHAbQYrKiKKG>1Fm>8Hyiz|-NYr|$ zW4Nk3slu-d;vmZ7E>4N>`2rHJn*;Xc7g&L}+1d1&!{MM6U{2~E>SgNZ%>8pt!>RS+ zF_E=GXRpARs)G}LqGUd01y(JKrH;X`%{gHMCo3+lohdX2p!n&=$b;p4El2aPc1cQ| zCcWAQMaZL|hE0(5&sgp{qGVcQj~PO{JZwF1En4UN5<@4JM2V}cEJGQx?wqfV0aFoS z&%9YfSgGRJdofk*t*F7^KrJu2^k|`9;(c)opytJTWbroA!ZinMB}SYfumB~8 z7c!3h5z89KP3M*r{&!JFu`5S1w`eSVys&P5&mLT->;`F)1)Y7XSB^QQ*aC`$ zLRn+80IK71Ssl#POOCg;(k>QS`-AlmLzv#+wdrHTBx+1{6!CY!7u}ANNWgTSN1bYJ zNEO}V2}pJ`1P0ixL(#uSGn?zQt zp-Q5k_t`KrF;@W31*`SirCJE8CU358;}`lwC~Z<$ES;m*cjD9ONC-8t#ev#&%zZN6 zCYT<>tovOq|I-f+q0_2qY5I}~+a-q4`)Ab$$y4YnoPz+X*uJGxz)x~xs zl#T2)@=z@LO&%F!DGYshXGqjiW)^S8J~sok)U6$=w2GYAW7W~Bi^jwjGA*Pf<*}aS zV=*i$UCa=$DoVbg8Q;me8)L7=-O4JLu_3sz)|ps^;-Y(OY>r`-3rSKeoV1Zhg|ka^ zA-V!!dK1E3VYAmfDUW-><-YLSyR}KBKz{fKP(&a_NH=>mtLLn0&EK%XZ)Su|MxGQ4@#DfVyCkU13L9wJC>F`HvZ2Y@X`}8Ln#_b|>>EcfV^<&7q z_nxZMeNGj+w{>EATo#I31TtKghDL3Or8E>08o(+Dv@DGdVr*_YH zLR+EsQPRq{ou(N7OyuSt1x)Kp_!8n&+a?R!s82c(Y8yb)9Y4x5;p5 zD9L!^LE{EPR@Ij3*-QIfORkhM%i4;;ALq#W4Ay)ZH3dwky$G$$7Z9~#=75ZGIv*0* zD5?ZCE(NtQfw$}HJfh!LS|A-)f=v>0Tu7xv|&H3`irHwt-@hi`oLc_ zIU-um&tTKhrvo6f0y4qynK$_ZkcgvxYV7*1QMDC{)YHh$xOP1nQcB^s7^$^aU4+H;U6+4F-`bR2JUjO!>Z)D8~Cd_D(e z=lfg{RLSHteM~NH!yV7A2jOHQ%`ktU*RyWcdpXrD)9o^ayMU@UC)bmdm{o5=O9pXG z#}~3U+>ZRgBG8*k38qkt%lgzZvA@571M68PGr$`s`0=2A9txv=kcx4oj!Dr6PbIM1 zv8dQ0N}4e2XoZ@iWqkEN2X5A{PG z1-(-T|H$yc0Le?JaQOUjx3(~x2EknsHkfF7rIpzZka?45a-uh0YwAQGL%$YUY-Z0i zVb+<)!t>Vr5J1VExYVwK-$YlAY`-y^d>W#WjK4@vzc%uHdiV~z@vuGJmgmd9*NAHDMsC`40-C1 z%egls`5xf()Yob;69OP-r>(q8-2S}o{0jasj)>Yatz=fxJjh5Rr^Di?eRN^sPK-8< zyr{HZU0S3l-%l8xtHQ(}4OyY&nS_YXk>i?u-X{h#r@b$TZ$)HORZY#Z0>SK->nYY# z1NT{t&GjOKEq1JLC-z8C!h;T}vODPP&UB+J=g7>cnvc=f6gy2Q2RP~!jv#i5=gwD*J1 zJqKCda@2c!uQ(?J5+sf926cwbmhuxK#n0$bICyIa*nH>d>T1$U6{HQ)Gu_F3b)HCQ zQVUh^b~>1HrDtVSY_8}+6rp)9G@kyMxk|psOe0Z-YV{XIo!kBl~u3#4xBRsgYw>L*sm_XioL!pShCYP2% zm5qfDfd*+80wS79ZU}DNF@rP9Sf!RP^C7mFgxkJmQwSV@;-llvY6GlU>wML${WJkb zZfNXW9PT#`oiI?xEz~&gM?zjW7j$Hsa5r&)bc4{9z98G|BDK6Ee}3Bq8?2=pUJy6q zwgXRyZsP<4Z357*!EqfL`j0gU^s_5O?MYX+$)z;#NUbs6T2l7cB>Cz*|Ab5)9liN2 z>yo-$E>c29N?-DIRq!SkV*o$P)C{DYpTUypI-hg;f%WQIn@I`R1e;l2PoDis%gC<9 zpQw($=DGUDLL&YolL?w!ZT&SIaZpEtKno!Ufn`VgV046$d@6)GOi@*O&Gc(HJazj@ z%{oL)-Qr7N3HsW+JIXCO7uQ(|LaSd!As??t0g6)dH$D2=2v`xr^c5j0bpUeL5wSot zjLmZ47gn#fzy12H&qJ0AbK{x&(F57I+E{~pF=qbegJKvN^#s-&7&AUhnex(FJzT~y zv^GQ6^FH%HAh25lHMNn{3dul6(V%6l*3PiSWwd?oyv3q0q_nme&)iJ1#{9H9VP{!* z0y>TQ!LWyK&Y*;eocO7-b4Il@Y%^YQ~?)k+1 z+2@}iQyViN2xIeVJuOLDDHHyiReG+XABY26`rh^S11`ZWq%>tv4Qo!cV%@y)}U5mU1B0D}XDD1ixZ-%p+(+Q3W zHtP1cC8ebIZ7)}CBk?n%+QRDl9tHRd@pmyAKIBMco{^_HViA+&v7v|h%* zE6{cog@|p>9=c|zZlYBR2MNNc(Zy&eZM?;@M-gn29zD_{n|R&mIr_p09xHWc#J*S> zFXzHIHH>9!<;n^!YPd1;;pa1jCN{x1bv;(r`L4Bic3fosreBjt=h>OLU9PRm;R8DR zkE>9Fm8iWU@8!@CT37py!}LzC#67_!xIvp7vY@tGF7d$;9+8xc{M^;6AX2Pv0)h|K z%RDq*$zV?($xQ|N`pL^{+Z3=sYs$O6-B>U{E-IKpTHdxH`3!+4H=M9M3rI-QtR{A- zC-B^4<0ibi5zXv)1#EUeOiF;+=bv?kdQ$GtE3Pz)@Z5_NEPcM0p%cNv)q|ruw#Ma$ za@7$8VNP)*rw?aIpT)EiTR&uzRr?%h^)$C=1Jf^H@7F>nRKIfEynX$m!vC3SyJPT{ z{A3Gw1Du}b^5FW$c1}`Qee7gUA6QDCq&$s{W5QW$tO~)kou_^lx4d(eG)CvL1_Cu~ zu32P&RH_?@%S5h{LcIaPGm9pxq+SK$r_;>AL%0SsbUwlbf6F7zGNPEdX1gP@jcg7S zW9R!h{@ENW;+aJS1?P+;T5E+Qb!?|C*HOV0j5jGzF%}bvrBnpbEZ9#<6cfOH6fPFP zuoq^l4W(FMo8Al@b|7tprYc=tpXtmZ>t+YNRb>|q$6N_W)+yT7e> zWE6tnR)qm5=yWY_XvFN@0OhjFGnV}gLjovPTdSQub(!xkcLveBsiXlof*`F7>q&k} z{c5+qjGU{m&F@)VqrM?B@J2=<#rB`Sd~Y|O8BoZhL&+F}EEN}U@ZdIIiFLSF4r@^^ z!b`kO$o5JdmHygboe(<9{?Xal9B3!_23|*&R&km)H$|25o>tiLb_nb6$x-Z!sE%MAHk)ymj zJkyJFb~y@pMxx>o3yup*kHhA1T9M+B(0#JEOw9^6AzW#JJT<;D5 z!~xqQNX6r8zAMkDxiINgqSKsf1#I?qO16!0s7zEA-d%v`w8OHh^6OE5yY9nRYo_d=lBy(oE zIA0xfv?@sHhudbOpmFd_EfT_`n5owh$V5Nsuef6#giTT?2)*>TojZnh-_zy3yuWz7 zQ_`d$&uSZ}eoaX?TcgrwR{`l~ZvW@dP!j#7TCMY@-Z5sfU+uR!rSSzj<^XqLy+G*H z-uG{Ns6G&`Sv7A0U8i2)crw{LwPa_QhIcS0I4E^q-h`hVViZ_5;@c~>q9h>fLW>0&!QQ<#Dm|rHdgUdFvzc%4I&Q~^tpZW-(Va2xyo8V7ILd z&VJIJHH=zpu6`yL*cDV_36>bnw6M0I^LGHn-^n^UKsrwk3%5dELesbeyqT}QY}l|L zX|75<*9bocb{WX#(Sv(L0tL$962hccqqJ0yw%N%X7n_{d*7~HqGSJT9fhNdXO)0N^ z9dEN@uP3SA)7cEla*{3R>?}I++p~|1Yia(Jc+)`&X5Rs3yTmW>IlE#v^Bfj!EK)nS_I39l&%=tSt6{>&~tAH}h!0 zcvgF=mlpG`s{%yK2NbdHYecevsCcp3^88Rip`w&wc_VoX35%~cjAs-d>}Lr1zF(8xe*zl=XlbA80? zGFEO5(VV@h)i{n<&tt1*q#Yn873RVhCZ}S}M0$G^RpS5^xG3FUyw5WY?92g)Y$t1Y zvzdq(-7peDshUXQM>?elQry})Y+c>(?bV-JauMNmkSGpT(ZMm!|9hciVfoP~ZRV2bBFdyNm&Rwp=HFkHJz=EYJF@{7G-`%$I?Io%+_i|Nl zWM;ovYL&n&cLbsR?VHbm^vY2-I61fM8yn7d4Gj#jB9_^N@ljj*L{rTEK{m=4P zv=<9)GxDbe@EY2&H4~U!O7a41X4|UXQny`o$`FY+7wL{7S#xpy?5R>B4FgxUCF@s}9nD_BU zmTRoa1fqCSOn&Lj%#y>Skk-Smk62|JP^5LVIJuR)lTVJJ1lDWVT+=j5ld->PsLJbL z^LVYa<3|`pu(*qB$ZRlCs{#-U+RH>hmWs`mL(m4#tX1&`Esx`OX~?{_PoB5|!Piy} zfLZ?GFdTx_*->(LP1!(aIG(x=nZ5pM+EY9VoT(v)#D3=2;0B9^9PZURa_nMInb4p)a& zJB6>|^-X}CD!Vc}FA>aL9&+ypOg3U%*1caIXofc1e{2)S9ybUJFUfmkMTN9{sk$WKtpnQW5^ z@cEpsDT`Nq)AbpGp-Icv=9~7ZFBjPxbfJIVmk@Pwc})aBzA$hqiE@lOaX5zle1=CK zHae&sV^3oAnn?q^I&HeyWi=`O((7r+&%&D1Ow9J1#Z+#8IlDozlngzbeH>+eSQ(s5 z&fY!5s?~>FGTqTYg`g}a3VNdc63#Sw@7v7G==oL!DS9sg^bG6AM#VmA)9XOw#>Me8 zNt<4$P}Vb$%bRDGxjp(?wdV_;9U~JcvK?joe)MYM^E%+Yap0!@@(k=GbnZj`1Ix7* zK1#E-RB>e3;vHuR3si0kBYrm@)&z0g>?gD+#h;XL+ee`%844wd{s_uGpcP|#e{g_v z^zGT96AT_RVRUYgTv4kb0JtHMPf3GF^T?)`RG`1lKa~#HCq}MBg@-YvQny5hujdH0 zE$=mAsa;i{sOw(Qp+H~@yM1j}n4D)eS~egYBsF(gGE`G(@6%^9%-&7i%>xnEdB)y6 zhIl|XyVudxCc_WGpiX7;T(Vs2(~D z#}=O*zW_2o&A;(}o}H_hbFt`>zG(}vs92ftKIg0*i1XC_N&s2|f|=^PE~Xi&d1zoH z;1YOncQlQ8u*8$F{mO8t8Dim>A6YpJ`(iS+WJYEjt*3(q^je5_AQ0V&%8Q(M)2a4m zpZql~EUfxY_Sdi9|M5>6(yg^-wC`fKS>AbFOdL3@dtnjPF!!W0%hP9O0|q$6-Jq@a z4Gi`MvP|;q-JS(u+y*<~61*|mUrV&8fF|KQ_2g6lvXn#iI|dE>i(=InxYUj<9H|{r z31=_++@K@>p$OWA7xt`epe&$NfP}C_^|K2E=hTXQMw3X*j~4}W8}*(0$W)YFVEP5z=Ol}! z)j9)ab@<8lYu>ZT-m^A>I|fM#nDCQMThhqoK>M&v7!_j3e7SJ#Ami#*GIzODK8Tv# z>Cb0P+3F^A4$Lj*)Ry`4ejbxtC4j?H_+%*k%5F2_$^~@m-pgZSX$`nGu}9F<3(e1JAy zk;iBoH+qLL0-)UH$aOu~1u_pcFBf^`_9s4sQi4zl1OWPZt8ruTZ_ldN>m2dVDH3J@ zzg}Z;RwBbvzbIBYi%j0xFb>5~3B=9nD}AnOdyyJBGbtnPds1!XZ@RASp(Q%bx&S8E@v5900K!^?@%SHwiifJ z!{6CaHGi8!BgOUHJ7EL8VURQlWy9RP;Z^rqb?WskA{6p+Hgo0jqf3M86%0~l^-Zd2 zc&^5Z_Y1~d(u5D`Re;G#%`F+(L4nL~xiiUI27)q;T~hT!S4g&7QSl_n0p7@@KrD;3 zo=yBE(SY5YZ#{pmn*KEh1W9%Kg{#B05~0<1O&jdEkfg~o-tdgOzQJGf5m!@R8xfmy zyQMsqWg~34Qq-N~9?o+QMiUTFxpfR@GK&NdEgG(9R_Kay^)qR6&lVCmDZFgJf4<~y zFzbVM$L6jC7Inw!M&(U}`=XSmCeP%S*{EI3vUQ2QNYGOYbF6jJwczM!zq?TwOLo&? zrWboGJ~T8cnL4>05A|dUgtNz(Q0lF5lx{+SZ#c!A2w>Z=SRUXrt!}b-0JFmM3e%F! zhqA}{qS9x4rmv?LGlLK8%POc zcm-|tpw~oE)FUXEMLlG?cI_U+{PlHLx&y#s1hCd7&jtw@qiJ#3gRIl5R%l!>bwvqM zYw2)z=Sd75G3|XVuA22i+d&TNvbv1f>8;ntoq5wJVL&Z70~*!5zIBu zVF;iad1fHeM`S9=`Z%Vz5U$j@Sn3X*d*0O0-{eXLmRm5mo7~vhcP#{$E0xg{*KCA5UB7( z|F6Hkd(u6g$cHaO>I&yxkn#nzm%|$Gryi6+mf6PpgH zt`Gz)*e!rKRz4q|o|O^vS?x{%i5u=5rfp?;k;-#e*nE>Zqj*?OlnI34ae;Ur3td^HVf>d8supy#nx^0PZowe2c8s@e>p66zeI9pz( zyKHft$8Cks@vNNTNEdkftjt54-bXR78U?iIBdU)b8YQe#6DIvJpJ7lIlez`` z=pMQ{xFufSbV^*U5crP!UKFpBmSMQ70Jy$15)or!(QRHZq2z1Rt;c+(s})M3JK|Xs z#d~uNKiLRpa0KFPel4l8*y@8#VuCmLEUkyL;kA#WaZ`I8i3RI46@8}G>377}^CAp% zvaGD3-G$82X8U7_IoF-_lav?;OqJI5b)XSJc@0E%7xdsxoi1`08SzbNr7!}z%ooE= z4TNa($J5RmRqiu0@1~_A#$fC~O@b!ZmK5pq5{bz|Kto&t*mMje+w@On2PyjQTEaZn znLav`>VDd3uV~K%RbL`xWqUmnAyDt-IB!7IAY(hpyj%-UfMKHGXst@RkaH!>QDA$q z5agfxQ{Hl8CoW`!FxIf#;Nu-O6kwheW?M5)fjmJu%Hy164#Rlr(@g zG%WQ8grU?sE}ph-Un~?+qD*>av-T8{7@eOa+b^Q9o@3)q(I;yCw=xTrl;Wh;h4s`H zr|AM|Vxh7=nf|)Bk&cnh%sq{ljo#``A>Xv#C!&+0B(> zRmD}QVQ^^zhZ**ZkbtI5nAfvx=^(-sz*a1s4S+}xUI>xd*%k^;At0omF*}6T%#+g+LPG?U>PM}ZK^|psWbV^%$0!l9kO_1^;t{RyFH!lPGNB%g_+!rpKv9X zRw(DIV|2;sGFVUjQJ%~`Pg#Qqk3X#@9!Q4Lsjv7p816AalUWl7xjl@Jg! zU?eTX-8J#B$+D0ZI-bv}9D$k^7j#8|p78f$-Y_$+otZ_>)6qQBn3(hNc*?1; zdWyNLYYitdigcPavnlc5?V7Vd@X`w)kaR~u{@0v0bx?17AfdxZ4aK4znKYMy1ZE=n zV?8^-tkaZTYZ7}kyysxdd>~6OHWfIb{o zlT%!sF}EV%B_wWzL^yX`i9ip76o;s`+IhO_PB~M!6jk54W~75_oGF&Qgrky9(D~wz z$`VSE&?d~(p30iSL1c>zSo4q58I&1^o6R$wNq2#Uk>s!kJ+qOYVCM@WejadQM^^od z*?t6sJXYgM{<0W;#X#D`0zn!-^ZAHYB0MpY=!`)diu7vPC%J@nK_^gS1`i&Jam8mg zs57{5jH9ZUWIaPMbiibz&htl6%!4alm0PdXDf2K7*P%<&_#2{!{JDKt(ps<^K|<-J z(h**<<@~H$;8_7>D(9#pO!qc$?xLn7r4Am5HZWf=-9^02KEMg@qC+iT-joUeB8rK- zMuvEYE8zf{P6nh#j&g#>Iqi((C|4;Yb!5TZ=vaJMaO$X*C?>-R2X)byQ?M3gG7i}= zt8r`-G4@jCY0OZl1(-F~sXmtA+1^?-Wow1y8=fqhgC#5fhX>xyd|qzPd%0$6V{N9` zCbU#J3`Qm;r~Vo))P=i1Gru`S0w+IYR+Aa#4H2{0W6d7iiBGuM1Z`{?>%-&M*5WJq z8@DY%{Da|M%p{#MJOsKo`ucA9&|ipFy!kb~7AvUO=W4DI*O4DR`HXCK>kZ7sRfx3w zD3iKx<_0#4LSdQlZPR3v0e&zv+V!rOou{&zle_S+FsEEf!tO40SQn>;_5#sT6BnrO zz-$txEM%ZCzy53yCjxQLWiToUCN5g(7y_&r0edlQsihKlNU5%UCcvcwLbX%p*ZZh% zLWuLh=44Y^cz$3Olx>3}Khq@@Pp)m5{^)D?4OnGnX$1EWRPXGxeX7;nLp?uBg-qHk-_fykb$ z`b^mDUp<3S2>SZztSnblJsB-RFort&>{d_-=cjW&7Bb@xDWtoo9aGVlWZLE)Gn6#+ zsO{A1G??BS!h6#}Y1P#7bI1iRAmjGVpMuGtogbjkk+$u@0 zu{aksc}BT;#fcox|pF)c7%8|xwRhD!e;9ZPJCv3-ho z=}k-&Q|g|0Q(f}NnC10cq#1{Z=OsZ`U)va5Av>QdKw@kUb)4);;RW0gum&>rq+X1Q(xK+iD!hz90aC9Z0fe?gS=1m7J%aaYZF% z6YCJAj@|5psPRF%^Rm%41MAxuPAeFDDc*XwS)%2@dp<27jn(xPq&g5>RyKD8!@75b zzAMhTNMAz`GBF%w8hNqH&z-{uv>?VJncL z4Z+9kQ6RLa1FN8*Nkjp(12;=QOtzV+4y{%l-i-w>MVRLiVneH4hqZQrsq&6v zh^qmU+u;q^IQZD8uIhkF^z8<1k+HFMt$BH@)i%I5>02y|2$)Wr%{P7&>@0xZ!EBeo zj4q4QP0_FuP3N`S@oKlP9p!l?_#&$#4PGt(;cpDi1Sp#!I>mNpo4aRJ$Y>ziaTfa8 zDQa@MHGaPMeB*7hOiQR9I9tH%Q)2wY&TsCD9rWlAZ z83$tCYjNZ_APxUT0vFU<=kAyShy|AHl)_RA#*%zJbE99?t-g46>w&jp^Z89Gh`|D= z-g#W1a&zrj|MOj&mX}K(DoQOCJWtd>VWMx2fdp#FbOCij+zf?^z>lDg7}?Yc!Lq8m z(ber)Zv)F zV#~*vsO)Ohmee9ny#0m}x18{hXX9^k=E88r@h6dd|&qn2$`rl!Yahv?Z8d8c#So zpy;J?Tn%RE`Y;|2&b|-PgDjd?%j9IleL{ztlMfrN?S)d_=JOz2OXZOz2gWbfO7XQFzK?JLOU74&~Fk@)d9oC&KXizh0($DyCAfKb8k`&b7klY-QvCJ;4 z6tbT|V{hY#n-Db`_8(+e-tR>&Ko^{B4>VA_JD5i5M+I!Nf0>BYY}dF^n6)x%5;YZU zvEOndPyToeY$Fh|+FW~UF24vkvN!2195GgLDz@~i`uR<(J&wn3o)Nn3kr1_1R`2+o zX;EelRZLxg9k1ExGtPFgELaLiio#WO7LxBosS`ZCiABPaGq5+z9;6AWlf(* zBov94wfZxW;VDaFq4g{M#j8qVTV}H8aY;_HNGNh$*SJzQ@aJKXuviyQdLeLfKs!uZ zND^0D{VdI|3BQ2UJ0Vp@(w|ySe1U8((zzb_%}av5zzA+G=Wj)HCQf}O9@|RHg)o*5 zA>Gk^_%PUFaE1z7jIuhM$QFjSagl2{_*%(iv!)~_pA__ERJwE0W~+Fy)pzZyW8DJE zTq&u0Gr_2ZQzDL%<%?88k<--h(;sZB68tH(u*~ja38!pBD+3+W;K`BRad%DS#^_Pz ztK+)o4(&9P=3Ts2c!vxK>a%llU?YNxcKSTo&~LX3Pz~lR+1YOq=6_sFk$Pu3kuDY4 zTrZj@n9uB(sFUNDYG(bi8Vn;#tXmFSF;B4;M5Og-3AF>r;c(yDd; z3YL5^)6Vq9QHo2iQE~UT^``y!JR0y(w!%0NyVY|$Gq-z3WI2a7BXiw{QGliZls)`Q zKbM<%o809!h?Yhxzx|EB2$F(KTjmrx$Fg#8HXtGx#?7myg&b$a%4H!qHCEC| zumaAJYE|Z=sL`}|>dZks{zXsA>?)n3oU{Ju0dj|RP$2(nHvs=Rl=0ZWgi!D+yo8fD zb|Eoe1P2Jw&P}2am54w6wnO_RD@VOpjLv6A_^4zBHk5YGrtmVuUk&}I^Hl?(B(`QF ze~rbil@>-rvpXPZT6)R|D{W$ZjX8li;pLKNey8sE002>hG$`or?8>Xt^f?;;jN|xl zC}C-uDfoMB0x?$+NqQqDBVAlr??*0JnL8)^txlXQ28|TDYw*llI}E6hpE6E@MXlz& zaadEv{)PlZ=&MfrhmxzhuNE1V!Cn2exzxg&gaC!SMY{^lkamu_E(Opq*QWcyo^#K4 ztWlfL0-kKrP>&IiSpy?lmt~=>ZX>_;g2q!~&ShYp!0#ofgjnmy0klg4ocFsntcwm( z8^JOvf*J2>d{*S!Y*Dy3;AXa#B{7^A_Mk+VczZ>OS^XLA-1KH-ipnTyHNLCS+%5Fu zq*ZA~zWlxm2bb=xy^#i6xvJ;bWv)>G-&&%vIPR>tG9aGUu;|unnNy!0jUA36F$A$d z1(3g_IxOx0lgv*cDGIT4$VkUfGDcWqr+8RnYVDPz&dn5=dJ^f3j!CjK(|KkE04u%P zreM=qU40o;z3w?50d-G*-K5k8`8hsKARY!mPj=&9x0?b$*?l#$Vc*h^nL0zG(jY(; z@2iQ@b96ybXDb!)(qL_6dRh!rFanjwEmN*1`HP69A;aEkuO%}(+wkLoyh zlcBMnvB!`l?bdUNgf=IsH7Wy)t}yn)A7PeqU+1H7Q50vF1JA_597Q}GG#sWS93ojM z-p$bg%it~vHAMr57Y!3h)-O_G3%7&5+i2bXYI5EQb4+!`PZlRcPmRvzHfZX*K-I~j zTldnG5bq>L-+T?bjLGece=fFe!!4lLf$Y55;(v7HYI~6)Dlt?idcTY|d#)G5|KPTp zkxZ5jVJVH;0yYuB@R`fPtgqT@)7o)mMI|rax@+h zf5oke0C{Q6Q=;tDw)+_DmC71J=)e&8K$*kB&FETH3ZT2d_}d+EC+wI0Kr`Fs8iA?7 z_n3y6$O!Nbz#a;T6XnH9Varlv?FH5VT6ml)s_J$Hv*_HaheaxKlg5?W#<($a_=T>V zh%I2OXHrh!l%&o!AQmr06Cvjq#}v)T3lk(rZP;$iVZ8TMpv#ma7Nz3QHYBZRkk)nzJ} z=}%tv%coFSQ{`s!uL5B@+4<*Qe6yRE>*}D%^NK4UW`c$@&(YRSf+?k)CN1kG3sI1> z*N;}@mG+;D)zE68;fZd$*%4~ddmL#X(X@8Anq$f(C`scs_kT922c{l^Q#DznGvW5! z2IaLNC>x77f@?O(k?A#roTsmV>LmT?A+i^oenz+Tu|#090bjyFVH%ANy7veZLp5$P zuIF8LR;yha26J_hPh=|`i`!JN4bG6QZwP056L+!)X+lW|HVs7}^t%8@<#>P&mI8>< z(Mgui`q@w5kVg!DMASg9dB)nJ2OMJLWnl47rk;V52MF3UJ+hO0#ZWZdO}1PR9%Ky# z#1PHCx~G08;1N`Lq?7IO_Md*I@;$gNmJzmOu$WLz8J*!>ZI~G~hw)%DEtG}df$`hf zC;1t`snXsB9(gzKiEHTb^hsj&jqS7)e-8VJ1Br(wBek(jp;&W>n*uYi@H#Z%q~{u< zP}5%mi4SZ|>CHau;HHkFoN>cY9UarqV{OzKJ4ku0gJo7Cy1%(?;Q$$#(VqqJ^lImO zx@F7FSaO%{Ol-BgaUl2n>=e1!TqV$+44{2S zg%l3>J-^C17NrQ^AB9&HDu4qt#7c1Hjhq;462LpYz*>3ABN0%yWu^mIG7;P&Hx2?b zzvtzOAH-goPD46Uyj_hO*sX^d^)&$86&*P#VxWd}aMqLniFa)c7y^2eq6MQx zLMJS&8Lunn6$qF7>{Umy=Bs?nf8&sPoU8;>t+_7s)=R3LZ8X(6RukzpkV2?0t%_PP zCXbE*Vrp?hY{ae*_@+J1z+TBK;<2M|f3XhdoA;K&rHGMwU4-RFTjw(*9o)spDkbGl ziU{RJ*H3n(VRq-ma1v#IHpkYTTXquwfv@g2Hq@GHAN7K`ZuqmG>nKd`;;J9*gGf>r zh>7J%bttI`0PPS)C|bZ>)`wR1>fBG8EL2>xyAoR>Qe2F>nM6SiyDk%0nOu^wU>0D? zuyzp3fDvdiY*Jnj$X`W{>Af3Au0N7p&!QDfT0=knIq$3j=t!zokoWWgN1DF=`1gP2 z$Y<6%HRU(DtXOL-_M&~2qViA()z(80=Q1IUx{Bq(6Sr1##Ok;aY8{~4;ERtAxj$rM z;2K#juza^F)PlG*rX_Ci;g_x z-Y=m)12Z{1K*ojmRIH`iS)RE?FCm>tpnr5RI@-Kf$OOG|dBTLs zlY$x$-OrrXV6lDkZ#H9&zf-@fkVuWVDRi8PgnARrb+N9M$F0PwgrNm58@r+lZ)I7} z4D(&st{$82tGSytS(b-(DVtRs`x#kg*rE)sm`$OAnRhp~oh4htyt29ayh>O#iCkdd z=X%D6cpmb(Za$lymi4;IW94o3aoBdeGtV@6ikhMh!Ew`(gtAtD_hQ1SIuq;$l*W3{kehiy=c&Nax zDcHsWig{nTen`_Y>miWL1xCDcGct;@CVnH}X1BGm!%yEu?UGeO4c|4bXHbP+?BcA@ z`iMdz2_1u(MYf8!K2BBAoC>cTtZ&nR)f-Pbvo4q=e;4d5N{nDe1rSVJ6{gkXFf#HX zOGfV9M=vz^7=3Dq<8v2V%Y&;nF!5h0v)qbd|9n| zK{21$Q^xw4sDL|GvZKoSYJA?+g##tAjrvi|5p=%OnHKlSahMkt2cX8CY+#t^Oe?jK z4jPsXAEnld-Q{tkss?CL${&m}LY8eIpCPI&@l3@;-()lR;;+hhm5q8Gm}e<1iZsW6 z*MwwcU``VZn?lG--|h1PwmQoE28#c5sLn^KLX#)*aQg+5wwed;EJH6T=_sdq6$+T} z`Mkn&l~S#mD9%vJa2`r6001S8Nkl%SXu!r`kjkY>cAnsIsyks@RKq znKsyjic)ymM`SjjirV*dh8c-%xZF$M)c}V8)|^4ytCbk80J9kT(5h9oKlKMvI_^;I zu*kiUK1=^~i9{qtGPRi8@tB^NtwnalYIY2=_OnN(N@16;RepT&T=hgiDj77!#g0>_ z6JghZL|;MNi?1froh~yf@2$aCxK(Fm>386)_n8GQx8>t+t!+Pho0i(ebq4ZdS4ViU ziFo3W|9HlvNmUKK@zsa|QpyuyVd}$dG9VUN2ik+L0JHC43R-w=Z zgIju<5BLnnEwE_=ZUnyk>=5`SRula1g%3teECY9xG`Y&RO?1{pnm5Y*!wc%|Kub?w zY4;`6Nq@_+=_M6`uU~)rqtiw2*3mizQ%f{-+A~h8f~eXyWa5Gqi)vE|MnG+fxAF;$J^_i>GG!;iXp3Ldi{1J!3Rku3r?q+*i=xr#7QvA<_7Hy=;l9Asg3pZYiei zOarwDMleIEO&}pJlf01|nUiiaBtx9COYl827OV$I)uq^$=Yx+Yf=XXL=Y*?zUMZy+6S4wfV+5a+@7JOT&7*PnP#&K znRfyva2{?v_N#q?C?<3{6n$F4^#>MSu%cF66SwN#!XGIle ztA8CNf#Ufl^g>In@WWg`G?znSMkp==(R0&;t=DH!cP6BGqrGw-H1mE(9oST?se{}WG#B-d6_ zqlHNq2n7znp}kZ+L=!awH-d25-W*dy#H`byb=l?qBZsWT8=(XN=q4FhQB#PhqDh&; z9K!1ZmtIoSr@Ffj^M+%Nibx%cVWw7)Z=^-C|NlyTL+WJj%;ar1EzB^JLUjBavfS)% zd6^z&vnY)+<`!&|7d6wx-kzNt#osbvFM$jJL0^D|N!Uh7yqKAN%FaSGgN3lR;}-SF zFcFq%YPUbp+(b|PyGrgbL)+K!@3ARn`gs{CzMyAOh1S7IUp+9D+BMXr<^;e~&tVNI_G1~N_Ry^yz675|!0O^KkE+4pi6 z7Bwu+dbGFfYX2KHr#~%-2sJNRIr}R>zpp5U5DaY!I9Cj4cUQbj?Pv|Z)`1TWYNMmF z1fwE=?j1(4WKd-ArAOUw@IHQ9Du} z=h;keV|7+wh!Tv^%}cbrzRjxPMJ$md7I67?{lJM{i0~ga|6PsB>Y@A=M)T0o20vQ1R&UU`*gHjR=LZSCz z9(rFgaPOp9LV6i_XTEJXq^ZRlj4fc1TFt=zLoj80N8g!{2 zMvtg+&)$;A28o?e39kFDZK01V=y@{cDx9AhIvz_ROqmp~UJY++Y6S)wB@~!YKA5Mg zW-eQcsW#KVW6h>Ws&!mpA^Gg5;|VTrR_5R|F`eAw|wmI(L}O`>5re=hrOq2a|`BfO_l$X2-Hok{-Z#A2U(z z6@d1U?8R*sgMwSq4Yy6!B5VYaI3_{sTw@y-bfGER$bLN}nAewUd1;KutySrnR^Os^ zCezM(_yiNqR1;$_^j3lgA*+Jt=_7In&a*$FnRHZC!Wh3W@LcEm&=un>{tnY+-;6Qq zC$cX<1v~tntY-SuSsvI;PXYQMvn>;=iF91m`~{OKP0M0Hu1T^)F#)i@k9fY}&X?Fv z{Q!nEI!Qft9i91FvN7Md^NWMUJMKAjB1>T!TMQ7n2DQ-DYK$2)C72v*&0l`~bJUU9 zdxpU332AQASd>n8H?zNp;$@gdL1ULV*P&<@n(^BKOJOtx>}F72RB;KPIU3m9Tdm8o z@{3!yUQ>p){0;Rx4SqmknoLita4V)Pipy3`8HBk&7&MpuiDy-WrZe>ES+~01&tk5i zIj$jcFXXgJik6|ZHY)wbMfyN3juK4eO|GtrmbF3*!ablHx)?Xn?DmJ=5awkVgOo^zjkVX}_ zKMG7w)L)fi4d0@UX@s`lO5qc2?@uATOHlmW&S~J#W(Lh)7L_TSw|kf^qWThEU3C(M z?KKVA${QSADh@FxC7KT55qhfhWNCG>Osp~lV^bc(SL}mU`&nq%gl17KJG)my0wg5? z++U6~d;zzUZ8tMga73@!%G++5Svz&Jf-z4)Dw7=Aqm32w8KQ!J&niaNqAoLE%N5}4mcn&_q#S`mh39Vp!O?Y!62 z%ML}WCYMRO>8||p&{Ica(GBI3LveKrfn$(9)8-;7@bbhkE6v*>F3;d_B+-Xi;Vhba zOXOB@yy_PVCC9b6>-%aY-GO3lix`*8)*LCHmA8B>cLkUY4&?4sS$Ymy1>bRpe|^s$ z@MoqnW@29&|CWg-h5dfpxpTOS6tkrXO^9e1 zGERDlh}#HyMbUO#oG~4~chUmM5wpFbEs<=}e5;Rn$Nrntn7A+2?m-7)lgM8g`lkT* zm$Bq)ub5d6UAz4ZLZde4w8tI`^uPXc{}Gsu**6&=%ZWW^OCv~cc(~*=nh0@1EIP^S z)#5gzP~+7V4Xu=1(@O_R{a!{uII)k^+ep4~;0=QBNRcUgzC?m)Ad+VD2i z`3Lv5n)2S-z?!4oQlEX~vx(70UWn!%z0!6_hRP9PvLp|7KO%@^Eqz}(wl6}OqC9kxt52i97X4P8faSV_1aQlj40Vy5_KbRitA znO$cdqlS_jY%0*d z$-I@)P)woJqx&S;m9ig3{@3?nh|mhVp!+^={uJOy=&f!bK`mtsZ|}UO5F&daijCh= zU(}V1fr&9^+iBi(9nW5C(RsD;Vsy5{T(TJ8l0jCxR+UO1_0pEru2gx)VY$;DryTzu zc0Vt41`X1{H02A@nNyUly(rpS^|+b`G?jp6Bh|HCIL50SF3lF!eL&s{fV_#kaY5+% ztKA?QntUQOwY<)9Vb&V4%6Ge3T_ac}ZiQ;O?j&JdxTu7%p-0?E5@Rb?X|CbjovY);qydA1qAomc`%l1dMaf)1uCx z4T42qCM?%0XWI(|SLGUHR|~PHzcd>GLTwBesdqBfgciq` ziv4p()Q__L`?ue{jL1q%w)9HR1nun_YcchF02}gR+x>NlQp5jks@B^+67kaD%^ZV*-EsDbrV0Q zcY<`7jo4APwn0COLKjql8``yEDoNiK)N_*=->2b2Dy%SeQ>2V*tR6`-=6VQ*!g*gY z8{Rz}-vXmX9Ifi%BnKDaa%`I+v^jN5iqm8$Apy1SgZ;}dSU@1T~&RSsr>_3-D z|5ZW<VW%&8|3^Yd-f!&|*X>AQbf)HXkVzNwQ(Utpq2+YNZo23{9c z&>o$QXa>#VctQvf@4noWt}|=~V2`w$8A3FI55of><*RIH&6_^YJ0eFu!{nZxE{dhP z8_}^qIx7LBiC7ObUeUJXbw$%f>k7+eb!O(qW4*YsMao^i*epK8PKhm2*|@&-jdr(b zMV)NTioXUDy;u=o_I;0L1MPCiDN1z)}AM3O;E86{g7e;biv5UPW)@h$SZ^;4=0>3`EFO5Zn zwOVfCIx5e0sm#|GPsdyWM;#WZ!QO)(^wa$fMrLt}kqeZ_Nm<38PtPtbO!g6^E0z^e zwPuuUzeSvq?hU~n8R=48MYFF@(8r+}3}Li>Rv$w z=p!1>{Om6Go2;jIO>tdeOC;rXdsx(IJ_6K8`DCONK9Y!da#8rUW!D!kEkGcz@47m*e|OKNd74WmK+I@QC@bHd0c{h7|?$ zDrualqAXE@ejEw-OC_+1PiL2pei#NT#gFw3cH^%%_gVyM^0Ct!K|el&{Dfj z$7tp9LT`QciZrsLAX7V-7s>pt+vOsnWLAyF@9MM;46`!HJPb`Tqj}7auyD1PC~C1{ z;F@o!To)moT{&2ixjpd7K5*DpB5p|IqfTPa;0VVhi%N+s?SsQg3Q+T#3;(u^{bbwL zO;eX;E_#LPYBgs(k-Iqt#vx`fmQm<(^|=IRw=~iy7SpcPdYVcu3fAcnTAb{7Y!`vP zE6tFt=e?Q(GplIy+-}%N2e)IUL{HX582v7>*k;1F7a3~sSAqh901wg-=z+acU>O6S{+*bBKB5g*rN_kz;Y^x1WX69gDd z+81PeRsW~#TZr&PlJ^m+aINd<3Xxg^*!{nkml>8vMO-$KyzsOKY1&#*_U#POxc4Ei z6?4Fb;icQra$5WokPrh4ru3&{HAkueAIoimpudR-WjlpfqSA+D=!s9LM2U~%t{D^A zs9frTub*36HGk9Fm=+vT~W5R)d2q!MQwoi^;a zaT&CO$*F-_atuGZbgF}9H1lPo9+5z{gL?{U*l!GaaSC>vJL)%`z0g>y)ur(ZI>DIH zUEbl`(+Xb>t}2b5=cXCZW5%-OqZEs$I4GNL^V?-7lU<+IXBJr$E+Fe5xEEaS-qml5 zK^m({*9TQtw!0l*YA%a;jtf{izw2kybI1hwu&ePpl9iQWn*=K~QW8OCxGR}s*eqx!HEKnR=>DjbQF5(^as|x z4R92XGcL9NnG=wCbeT>|DxS+xCYvl%e=akz4jTsFPtyQq#d11lU(iatqVXo*EJd za#1UMb4~Eo(&ITd`QpG@Y_1*pJWoSvAag1F|Tcgt1M~o@?+*hlIk!A=w4$O` zqAxm*dwnRe!i^gCO~@o@i(fA{&v<#AYMsjOb%EnLcv>00AFs+!i!N6@hdHDiX)P%- z!dkq+InSyg(r@PgG3^@GQ^J7=vL?vgR|f0QoG7H>Vox?EuQ^U50V|0L%I&0I8=JXA z^SPx1cRvKTBIcpv0i6h9_c96r3(aEX2)q3UC{7VHKW5VKz97c z&3Gq=-C2dByp8mkVRh_eotk-Lz9l)G>V*X*Z!z*&>x-;F8JXxad!e87JrDDAZWfH_ zt4DCf`G-)0i`MQ^sS-5_U_xm@yOy`n*?^Nn-B<%^}H zk`I&o3?k~e{gV6E(yi(Urf!gW|$tNX#m z+LZfYr*zwrwo}y#TgMc-oRG%R2f2Zln@gHevnixyX7q;n^VsQ53a5Cg#*dMcf>n2GOf2BF0$!qx$!EiefdpUCqP4CN4U39ZZ~2g6$xXLw8| z9N$Qn;Znhe20=8^J8x^wm+0P-4ZDZ}aU822w(gT+8K;p}Y;xRD>E9^LXE|jYwk;Qk zk6oc)P0?l7nK>GdQqMOV&X)QTm53dGtJLuRu1Vy)g-5Yy1Yte9k?9Mf*4Yu=EUT{i z&I`4uIb>noBS`2?&`w@H6k@+a*{P~by0GE+XbZ!{y6f@twipa_d+LHZS5e!Lqz-btdvI>6K>Ws=Zs~Mq zbzAc)%9-(na-elodTyg^`%uZVRlO$7WIo2hw-RE}=hR(zRV8)UXABw^wqC)YOIFKlOf9I2`?uBP*|dtvh!* z%Ow^;Koce^8LHbRIjhkDe#zMimr1UNXYgwsF7{lcu$70QJM{BB2 zQ<9Q-A1i6f{Ow06)gdoyfyj;aD8S0I`)M5@9}8Uec@TJ{?ncQ^z`Eou0pCDaEt$lp zBp&XkF}#P>r(hGPhs+vUzKrEP`D4;Y%XrN=g4K73n^|)%FT|fpc>vGarC@kZrKO8z z=jQc^Q+Ff#Z}NK1*L08QXWBX)Ujp5A!yY15F&3waxLnAF5Lq|hRBvpHQCYkGVyo032Z5387VuIEJ`B`?s!b&LYNw_+dPRVvL6YqoW0#HPmNd|#kb9dX*@N0!j#qU|mU~wB z-_m5d4P&FUg{(I2q^7@zHSJ#I?d3kJ^-!62U7&X(z&(z}vhrh40e`E*3nIiO3BuC| zr;SdIxU0J?1eiz(soiMYk(s!A_r!ix-S+lwQf^~94Q9H2bMvR0>e0t@uN{-d+8yf> z1(uP8KKgjVDDD0sWn#J{=@TY@zq7L4p}8jL)E8a@Xn+i$GAtbft%GzO+2k@eC!xLO z(7xS-l>9-jE|d2JI{{Gm@>g$G(6FH(;$K!;yif~k896C(2Lfd5an^-d`Wq>Up=^P8 zsK@b~F)AHippLhxE=kiHFRVi%Mr3J?%E>aLiD2+_CeyNr->6&+I`!FHvx>UZIZa4S z-iZ=iCLQP|xA&XKc>?Q`3JJw9h^Neq{b7{Oe71(W8<2#+wWnB3FCqvzSne-avhu7gg>&;1t2VVA)Kt1(r z|KVtHzg74Kwh87VwdU;=ejd~}_`&f0tST>V)KXgZ@S~go;c>g(k*ahEzqY)}$cuo* zofNqYpY4(J^6NQ?gxbRE*=!ivJQ8k5&(pW8J z<$)3?HW}>~;ip8h<KAZjqAk6?#v?&`*AeyDF-?`irkFOE8j%?NY=1;0M>u~F=BPxnS>G^vqJ zJ`m(#+d>)Kq5-l8xPiyPSJwDRr+>F?H3-h8$gb__waxxc`1nXW4_M6UrjGg{Ix>@PZ6zmPeAfg)X==ES-)4K+6Verwwbkhlk?{rSQh8afL zE7i`)h)Dxc{``Bn=j;8HuEf(EO_pxsR?bM%RsjbOw6CNpNwz_D!II&r-`OyViRpf- zyDcFGmCB+u4=f_h55oV45M-yj0B4bXXSgnw0>-k|g7i2x@6hX^TRqhvb8_W*EtA3i zF3jEREMikjQG+B@gq|wB2c}<4%N0a5EPnHm*_PF7(VBXHzUkSxqZDi)5nk138oILd zkuExn-Gjv^gu)feJ+6M^0;H!^|~SH_}q-F+x4n=k{uaGkN`|sQG0O=2A9Z pKkGT8F|V)#SPt6()hl{Y`XA~4@3XhK(5V0b002ovPDHLkV1l8T*U + { + 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/UmbraAPI/MareSynchronosAPI/Data/ChatMessage.cs b/UmbraAPI/MareSynchronosAPI/Data/ChatMessage.cs new file mode 100644 index 0000000..55224a5 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs b/UmbraAPI/MareSynchronosAPI/Data/Comparer/GroupDataComparer.cs new file mode 100644 index 0000000..dfd0456 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs b/UmbraAPI/MareSynchronosAPI/Data/Comparer/GroupDtoComparer.cs new file mode 100644 index 0000000..3814c6f --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs b/UmbraAPI/MareSynchronosAPI/Data/Comparer/GroupPairDtoComparer.cs new file mode 100644 index 0000000..c1dde50 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Comparer/UserDataComparer.cs b/UmbraAPI/MareSynchronosAPI/Data/Comparer/UserDataComparer.cs new file mode 100644 index 0000000..68aa227 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs b/UmbraAPI/MareSynchronosAPI/Data/Comparer/UserDtoComparer.cs new file mode 100644 index 0000000..9c8451c --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Enum/GroupPermissions.cs b/UmbraAPI/MareSynchronosAPI/Data/Enum/GroupPermissions.cs new file mode 100644 index 0000000..cccc712 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Enum/GroupUserInfo.cs b/UmbraAPI/MareSynchronosAPI/Data/Enum/GroupUserInfo.cs new file mode 100644 index 0000000..ed1b3bb --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs b/UmbraAPI/MareSynchronosAPI/Data/Enum/GroupUserPermissions.cs new file mode 100644 index 0000000..efa3bfd --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Enum/MessageSeverity.cs b/UmbraAPI/MareSynchronosAPI/Data/Enum/MessageSeverity.cs new file mode 100644 index 0000000..b0ace02 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Enum/ObjectKind.cs b/UmbraAPI/MareSynchronosAPI/Data/Enum/ObjectKind.cs new file mode 100644 index 0000000..47396c4 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Enum/UserPermissions.cs b/UmbraAPI/MareSynchronosAPI/Data/Enum/UserPermissions.cs new file mode 100644 index 0000000..8cc472b --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Extensions/GroupPermissionsExtensions.cs b/UmbraAPI/MareSynchronosAPI/Data/Extensions/GroupPermissionsExtensions.cs new file mode 100644 index 0000000..ca2236d --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs b/UmbraAPI/MareSynchronosAPI/Data/Extensions/GroupUserInfoExtensions.cs new file mode 100644 index 0000000..a4608e8 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Extensions/GroupUserPermissionsExtensions.cs b/UmbraAPI/MareSynchronosAPI/Data/Extensions/GroupUserPermissionsExtensions.cs new file mode 100644 index 0000000..b8b2702 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/Extensions/UserPermissionsExtensions.cs b/UmbraAPI/MareSynchronosAPI/Data/Extensions/UserPermissionsExtensions.cs new file mode 100644 index 0000000..2b80601 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/FileReplacementData.cs b/UmbraAPI/MareSynchronosAPI/Data/FileReplacementData.cs new file mode 100644 index 0000000..82161a5 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/GroupData.cs b/UmbraAPI/MareSynchronosAPI/Data/GroupData.cs new file mode 100644 index 0000000..877bb44 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/SignedChatMessage.cs b/UmbraAPI/MareSynchronosAPI/Data/SignedChatMessage.cs new file mode 100644 index 0000000..edfd8cc --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Data/UserData.cs b/UmbraAPI/MareSynchronosAPI/Data/UserData.cs new file mode 100644 index 0000000..3bc74cf --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Account/RegisterReplyDto.cs new file mode 100644 index 0000000..ce3f741 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Account/RegisterReplyV2Dto.cs new file mode 100644 index 0000000..59f7fe5 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/AuthReplyDto.cs new file mode 100644 index 0000000..d3033fd --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs new file mode 100644 index 0000000..9c53eaa --- /dev/null +++ b/UmbraAPI/MareSynchronosAPI/Dto/CharaData/AccessTypeDto.cs @@ -0,0 +1,9 @@ +namespace MareSynchronos.API.Dto.CharaData; + +public enum AccessTypeDto +{ + Individuals, + ClosePairs, + AllPairs, + Public +} diff --git a/UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDownloadDto.cs new file mode 100644 index 0000000..5d450b8 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataDto.cs new file mode 100644 index 0000000..dbf4a26 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataFullDto.cs new file mode 100644 index 0000000..d8b4016 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataMetaInfoDto.cs new file mode 100644 index 0000000..7afb6b2 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/CharaData/CharaDataUpdateDto.cs new file mode 100644 index 0000000..30d1348 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs new file mode 100644 index 0000000..ed55f94 --- /dev/null +++ b/UmbraAPI/MareSynchronosAPI/Dto/CharaData/ShareTypeDto.cs @@ -0,0 +1,7 @@ +namespace MareSynchronos.API.Dto.CharaData; + +public enum ShareTypeDto +{ + Private, + Shared +} diff --git a/UmbraAPI/MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Chat/GroupChatMsgDto.cs new file mode 100644 index 0000000..c946c00 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Chat/UserChatMsgDto.cs new file mode 100644 index 0000000..d82855b --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/ConnectionDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/ConnectionDto.cs new file mode 100644 index 0000000..04c818e --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Files/DownloadFileDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Files/DownloadFileDto.cs new file mode 100644 index 0000000..d2ffe05 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Files/FilesSendDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Files/FilesSendDto.cs new file mode 100644 index 0000000..b7a6735 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Files/ITransferFileDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Files/ITransferFileDto.cs new file mode 100644 index 0000000..fb20e5a --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Files/UploadFileDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Files/UploadFileDto.cs new file mode 100644 index 0000000..f10b27d --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Group/BannedGroupUserDto.cs new file mode 100644 index 0000000..36ed1f9 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupDto.cs new file mode 100644 index 0000000..5b5b71a --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupFullInfoDto.cs new file mode 100644 index 0000000..0591293 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupInfoDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupInfoDto.cs new file mode 100644 index 0000000..193072b --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPairDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPairDto.cs new file mode 100644 index 0000000..c2e748d --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPairFullInfoDto.cs new file mode 100644 index 0000000..5a594df --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPairUserInfoDto.cs new file mode 100644 index 0000000..8a37f68 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPairUserPermissionDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPairUserPermissionDto.cs new file mode 100644 index 0000000..d1f152f --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPasswordDto.cs new file mode 100644 index 0000000..bcc31f0 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/Group/GroupPermissionDto.cs new file mode 100644 index 0000000..70dbf80 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/SystemInfoDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/SystemInfoDto.cs new file mode 100644 index 0000000..eb84f1a --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/User/OnlineUserCharaDataDto.cs new file mode 100644 index 0000000..a4233d5 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/User/OnlineUserIdentDto.cs new file mode 100644 index 0000000..dbc7129 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/User/UserCharaDataMessageDto.cs new file mode 100644 index 0000000..1b33590 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/User/UserDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/User/UserDto.cs new file mode 100644 index 0000000..ce105bf --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/User/UserPairDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/User/UserPairDto.cs new file mode 100644 index 0000000..3d92ad6 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/User/UserPermissionsDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/User/UserPermissionsDto.cs new file mode 100644 index 0000000..772040b --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/User/UserProfileDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/User/UserProfileDto.cs new file mode 100644 index 0000000..0b103e5 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Dto/User/UserProfileReportDto.cs b/UmbraAPI/MareSynchronosAPI/Dto/User/UserProfileReportDto.cs new file mode 100644 index 0000000..02ed9ef --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/MareSynchronos.API.csproj b/UmbraAPI/MareSynchronosAPI/MareSynchronos.API.csproj new file mode 100644 index 0000000..44e5fc8 --- /dev/null +++ b/UmbraAPI/MareSynchronosAPI/MareSynchronos.API.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/UmbraAPI/MareSynchronosAPI/MareSynchronosAPI.sln b/UmbraAPI/MareSynchronosAPI/MareSynchronosAPI.sln new file mode 100644 index 0000000..ffde134 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Routes/MareAuth.cs b/UmbraAPI/MareSynchronosAPI/Routes/MareAuth.cs new file mode 100644 index 0000000..2bef31e --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/Routes/MareFiles.cs b/UmbraAPI/MareSynchronosAPI/Routes/MareFiles.cs new file mode 100644 index 0000000..a4e5f5d --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/SignalR/IMareHub.cs b/UmbraAPI/MareSynchronosAPI/SignalR/IMareHub.cs new file mode 100644 index 0000000..7475116 --- /dev/null +++ b/UmbraAPI/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/UmbraAPI/MareSynchronosAPI/SignalR/IMareHubClient.cs b/UmbraAPI/MareSynchronosAPI/SignalR/IMareHubClient.cs new file mode 100644 index 0000000..d13cbaa --- /dev/null +++ b/UmbraAPI/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

|^ZF3r4%v8xKJLk}v!T)ENU=abH5AepeO@R+ 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%0D=9T3(S>U7;*b}tt~=ef+P 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